Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dda2854be | ||
|
|
5184ed6fc7 | ||
|
|
4e679f776e | ||
|
|
96328f66e9 | ||
|
|
3eb14ab8a1 | ||
|
|
c6266b7917 | ||
|
|
6c27f2e2f1 | ||
|
|
e04a0094b1 | ||
|
|
683e6a58ea | ||
|
|
b340feeb49 | ||
|
|
6a39b7ef5a |
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -6,3 +6,9 @@
|
|||||||
path = plugins/flutter_distributor
|
path = plugins/flutter_distributor
|
||||||
url = git@github.com:chen08209/flutter_distributor.git
|
url = git@github.com:chen08209/flutter_distributor.git
|
||||||
branch = FlClash
|
branch = FlClash
|
||||||
|
[submodule "plugins/tray_manager"]
|
||||||
|
path = plugins/tray_manager
|
||||||
|
url = git@github.com:chen08209/tray_manager.git
|
||||||
|
branch = main
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,3 +1,63 @@
|
|||||||
|
## v0.8.77
|
||||||
|
|
||||||
|
- Optimize performance
|
||||||
|
|
||||||
|
- Update core
|
||||||
|
|
||||||
|
- Optimize core stability
|
||||||
|
|
||||||
|
- Fix linux tun authority check error
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Fix scroll physics error
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.75
|
||||||
|
|
||||||
|
- Add windows storage corruption detection
|
||||||
|
|
||||||
|
- Fix core crash caused by windows resource manager restart
|
||||||
|
|
||||||
|
- Optimize logs, requests, access to pages
|
||||||
|
|
||||||
|
- Fix macos bypass domain issues
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.74
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.73
|
||||||
|
|
||||||
|
- Update popup menu
|
||||||
|
|
||||||
|
- Add file editor
|
||||||
|
|
||||||
|
- Fix android service issues
|
||||||
|
|
||||||
|
- Optimize desktop background performance
|
||||||
|
|
||||||
|
- Optimize android main process performance
|
||||||
|
|
||||||
|
- Optimize delay test
|
||||||
|
|
||||||
|
- Optimize vpn protect
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.72
|
||||||
|
|
||||||
|
- Update core
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
## v0.8.71
|
## v0.8.71
|
||||||
|
|
||||||
- Remake dashboard
|
- Remake dashboard
|
||||||
|
|||||||
@@ -1,29 +1,8 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints.
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at
|
|
||||||
# https://dart-lang.github.io/linter/lints/index.html.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
analyzer:
|
||||||
# https://dart.dev/guides/language/analysis-options
|
plugins:
|
||||||
|
- custom_lint
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name=".FlClashApplication"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="FlClash">
|
android:label="FlClash">
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.follow.clash;
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
class FlClashApplication : Application() {
|
||||||
|
companion object {
|
||||||
|
private lateinit var instance: FlClashApplication
|
||||||
|
fun getAppContext(): Context {
|
||||||
|
return instance.applicationContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ object GlobalState {
|
|||||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getText(text: String): String {
|
suspend fun getText(text: String): String {
|
||||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
return getCurrentAppPlugin()?.getText(text) ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,14 +44,14 @@ object GlobalState {
|
|||||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleToggle(context: Context) {
|
fun handleToggle() {
|
||||||
val starting = handleStart(context)
|
val starting = handleStart()
|
||||||
if (!starting) {
|
if (!starting) {
|
||||||
handleStop()
|
handleStop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleStart(context: Context): Boolean {
|
fun handleStart(): Boolean {
|
||||||
if (runState.value == RunState.STOP) {
|
if (runState.value == RunState.STOP) {
|
||||||
runState.value = RunState.PENDING
|
runState.value = RunState.PENDING
|
||||||
runLock.lock()
|
runLock.lock()
|
||||||
@@ -59,7 +59,7 @@ object GlobalState {
|
|||||||
if (tilePlugin != null) {
|
if (tilePlugin != null) {
|
||||||
tilePlugin.handleStart()
|
tilePlugin.handleStart()
|
||||||
} else {
|
} else {
|
||||||
initServiceEngine(context)
|
initServiceEngine()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -74,6 +74,12 @@ object GlobalState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleTryDestroy() {
|
||||||
|
if (flutterEngine == null) {
|
||||||
|
destroyServiceEngine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun destroyServiceEngine() {
|
fun destroyServiceEngine() {
|
||||||
runLock.withLock {
|
runLock.withLock {
|
||||||
serviceEngine?.destroy()
|
serviceEngine?.destroy()
|
||||||
@@ -81,21 +87,21 @@ object GlobalState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initServiceEngine(context: Context) {
|
fun initServiceEngine() {
|
||||||
if (serviceEngine != null) return
|
if (serviceEngine != null) return
|
||||||
destroyServiceEngine()
|
destroyServiceEngine()
|
||||||
runLock.withLock {
|
runLock.withLock {
|
||||||
serviceEngine = FlutterEngine(context)
|
serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
|
||||||
serviceEngine?.plugins?.add(VpnPlugin())
|
serviceEngine?.plugins?.add(VpnPlugin)
|
||||||
serviceEngine?.plugins?.add(AppPlugin())
|
serviceEngine?.plugins?.add(AppPlugin())
|
||||||
serviceEngine?.plugins?.add(TilePlugin())
|
serviceEngine?.plugins?.add(TilePlugin())
|
||||||
serviceEngine?.plugins?.add(ServicePlugin())
|
|
||||||
val vpnService = DartExecutor.DartEntrypoint(
|
val vpnService = DartExecutor.DartEntrypoint(
|
||||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||||
"vpnService"
|
"_service"
|
||||||
)
|
)
|
||||||
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
||||||
vpnService,
|
vpnService,
|
||||||
|
if (flutterEngine == null) listOf("quick") else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.follow.clash
|
package com.follow.clash
|
||||||
|
|
||||||
|
|
||||||
import com.follow.clash.plugins.AppPlugin
|
import com.follow.clash.plugins.AppPlugin
|
||||||
import com.follow.clash.plugins.ServicePlugin
|
import com.follow.clash.plugins.ServicePlugin
|
||||||
import com.follow.clash.plugins.TilePlugin
|
import com.follow.clash.plugins.TilePlugin
|
||||||
import com.follow.clash.plugins.VpnPlugin
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
@@ -12,8 +10,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.plugins.add(AppPlugin())
|
flutterEngine.plugins.add(AppPlugin())
|
||||||
flutterEngine.plugins.add(VpnPlugin())
|
flutterEngine.plugins.add(ServicePlugin)
|
||||||
flutterEngine.plugins.add(ServicePlugin())
|
|
||||||
flutterEngine.plugins.add(TilePlugin())
|
flutterEngine.plugins.add(TilePlugin())
|
||||||
GlobalState.flutterEngine = flutterEngine
|
GlobalState.flutterEngine = flutterEngine
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class TempActivity : Activity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
wrapAction("START") -> {
|
wrapAction("START") -> {
|
||||||
GlobalState.handleStart(applicationContext)
|
GlobalState.handleStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapAction("STOP") -> {
|
wrapAction("STOP") -> {
|
||||||
@@ -17,7 +17,7 @@ class TempActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wrapAction("CHANGE") -> {
|
wrapAction("CHANGE") -> {
|
||||||
GlobalState.handleToggle(applicationContext)
|
GlobalState.handleToggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ fun String.toCIDR(): CIDR {
|
|||||||
return CIDR(address, prefixLength)
|
return CIDR(address, prefixLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||||
val properties = getLinkProperties(network) ?: return listOf()
|
val properties = getLinkProperties(network) ?: return listOf()
|
||||||
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
||||||
@@ -143,7 +142,6 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun numericToTextFormat(src: ByteArray): String {
|
private fun numericToTextFormat(src: ByteArray): String {
|
||||||
val sb = StringBuilder(39)
|
val sb = StringBuilder(39)
|
||||||
for (i in 0 until 8) {
|
for (i in 0 until 8) {
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ data class Package(
|
|||||||
val packageName: String,
|
val packageName: String,
|
||||||
val label: String,
|
val label: String,
|
||||||
val isSystem: Boolean,
|
val isSystem: Boolean,
|
||||||
val firstInstallTime: Long,
|
val lastUpdateTime: Long,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.follow.clash.models
|
package com.follow.clash.models
|
||||||
|
|
||||||
data class Process(
|
data class Process(
|
||||||
val id: Int,
|
val id: String,
|
||||||
val metadata: Metadata,
|
val metadata: Metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ enum class AccessControlMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class AccessControl(
|
data class AccessControl(
|
||||||
|
val enable: Boolean,
|
||||||
val mode: AccessControlMode,
|
val mode: AccessControlMode,
|
||||||
val acceptList: List<String>,
|
val acceptList: List<String>,
|
||||||
val rejectList: List<String>,
|
val rejectList: List<String>,
|
||||||
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
|
|||||||
data class VpnOptions(
|
data class VpnOptions(
|
||||||
val enable: Boolean,
|
val enable: Boolean,
|
||||||
val port: Int,
|
val port: Int,
|
||||||
val accessControl: AccessControl?,
|
val accessControl: AccessControl,
|
||||||
val allowBypass: Boolean,
|
val allowBypass: Boolean,
|
||||||
val systemProxy: Boolean,
|
val systemProxy: Boolean,
|
||||||
val bypassDomain: List<String>,
|
val bypassDomain: List<String>,
|
||||||
@@ -25,4 +26,9 @@ data class VpnOptions(
|
|||||||
val ipv4Address: String,
|
val ipv4Address: String,
|
||||||
val ipv6Address: String,
|
val ipv6Address: String,
|
||||||
val dnsServerAddress: String,
|
val dnsServerAddress: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StartForegroundParams(
|
||||||
|
val title: String,
|
||||||
|
val content: String,
|
||||||
)
|
)
|
||||||
@@ -3,7 +3,6 @@ package com.follow.clash.plugins
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.ComponentInfo
|
import android.content.pm.ComponentInfo
|
||||||
@@ -19,6 +18,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
|
|||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||||
|
import com.follow.clash.FlClashApplication
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.R
|
import com.follow.clash.R
|
||||||
import com.follow.clash.extensions.awaitResult
|
import com.follow.clash.extensions.awaitResult
|
||||||
@@ -37,16 +37,14 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activityRef: WeakReference<Activity>? = null
|
||||||
|
|
||||||
private lateinit var context: Context
|
|
||||||
|
|
||||||
private lateinit var channel: MethodChannel
|
private lateinit var channel: MethodChannel
|
||||||
|
|
||||||
@@ -121,21 +119,27 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
scope = CoroutineScope(Dispatchers.Default)
|
scope = CoroutineScope(Dispatchers.Default)
|
||||||
context = flutterPluginBinding.applicationContext
|
|
||||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||||
channel.setMethodCallHandler(this)
|
channel.setMethodCallHandler(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initShortcuts(label: String) {
|
private fun initShortcuts(label: String) {
|
||||||
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
|
val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
|
||||||
.setShortLabel(label)
|
.setShortLabel(label)
|
||||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
|
.setIcon(
|
||||||
.setIntent(context.getActionIntent("CHANGE"))
|
IconCompat.createWithResource(
|
||||||
|
FlClashApplication.getAppContext(),
|
||||||
|
R.mipmap.ic_launcher_round
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
|
||||||
.build()
|
.build()
|
||||||
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
|
ShortcutManagerCompat.setDynamicShortcuts(
|
||||||
|
FlClashApplication.getAppContext(),
|
||||||
|
listOf(shortcut)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
channel.setMethodCallHandler(null)
|
channel.setMethodCallHandler(null)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
@@ -143,14 +147,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
private fun tip(message: String?) {
|
private fun tip(message: String?) {
|
||||||
if (GlobalState.flutterEngine == null) {
|
if (GlobalState.flutterEngine == null) {
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"moveTaskToBack" -> {
|
"moveTaskToBack" -> {
|
||||||
activity?.moveTaskToBack(true)
|
activityRef?.get()?.moveTaskToBack(true)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +196,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
if (iconMap["default"] == null) {
|
if (iconMap["default"] == null) {
|
||||||
iconMap["default"] =
|
iconMap["default"] =
|
||||||
context.packageManager?.defaultActivityIcon?.getBase64()
|
FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
|
||||||
}
|
}
|
||||||
result.success(iconMap["default"])
|
result.success(iconMap["default"])
|
||||||
return@launch
|
return@launch
|
||||||
@@ -221,8 +225,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
private fun openFile(path: String) {
|
private fun openFile(path: String) {
|
||||||
val file = File(path)
|
val file = File(path)
|
||||||
val uri = FileProvider.getUriForFile(
|
val uri = FileProvider.getUriForFile(
|
||||||
context,
|
FlClashApplication.getAppContext(),
|
||||||
"${context.packageName}.fileProvider",
|
"${FlClashApplication.getAppContext().packageName}.fileProvider",
|
||||||
file
|
file
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -234,13 +238,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
val flags =
|
val flags =
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
|
||||||
val resInfoList = context.packageManager.queryIntentActivities(
|
val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities(
|
||||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
for (resolveInfo in resInfoList) {
|
for (resolveInfo in resInfoList) {
|
||||||
val packageName = resolveInfo.activityInfo.packageName
|
val packageName = resolveInfo.activityInfo.packageName
|
||||||
context.grantUriPermission(
|
FlClashApplication.getAppContext().grantUriPermission(
|
||||||
packageName,
|
packageName,
|
||||||
uri,
|
uri,
|
||||||
flags
|
flags
|
||||||
@@ -248,19 +252,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activity?.startActivity(intent)
|
activityRef?.get()?.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println(e)
|
println(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||||
val am = getSystemService(context, ActivityManager::class.java)
|
val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java)
|
||||||
val task = am?.appTasks?.firstOrNull {
|
val task = am?.appTasks?.firstOrNull {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
it.taskInfo.taskId == activity?.taskId
|
it.taskInfo.taskId == activityRef?.get()?.taskId
|
||||||
} else {
|
} else {
|
||||||
it.taskInfo.id == activity?.taskId
|
it.taskInfo.id == activityRef?.get()?.taskId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +276,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPackageIcon(packageName: String): String? {
|
private suspend fun getPackageIcon(packageName: String): String? {
|
||||||
val packageManager = context.packageManager
|
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||||
if (iconMap[packageName] == null) {
|
if (iconMap[packageName] == null) {
|
||||||
iconMap[packageName] = try {
|
iconMap[packageName] = try {
|
||||||
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
||||||
@@ -285,10 +289,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackages(): List<Package> {
|
private fun getPackages(): List<Package> {
|
||||||
val packageManager = context.packageManager
|
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||||
if (packages.isNotEmpty()) return packages
|
if (packages.isNotEmpty()) return packages
|
||||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||||
it.packageName != context.packageName
|
it.packageName != FlClashApplication.getAppContext().packageName
|
||||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||||
|| it.packageName == "android"
|
|| it.packageName == "android"
|
||||||
|
|
||||||
@@ -297,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
packageName = it.packageName,
|
packageName = it.packageName,
|
||||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||||
firstInstallTime = it.firstInstallTime
|
lastUpdateTime = it.lastUpdateTime
|
||||||
)
|
)
|
||||||
}?.let { packages.addAll(it) }
|
}?.let { packages.addAll(it) }
|
||||||
return packages
|
return packages
|
||||||
@@ -317,43 +321,45 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
|
fun requestVpnPermission(callBack: () -> Unit) {
|
||||||
vpnCallBack = callBack
|
vpnCallBack = callBack
|
||||||
val intent = VpnService.prepare(context)
|
val intent = VpnService.prepare(FlClashApplication.getAppContext())
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
vpnCallBack?.invoke()
|
vpnCallBack?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestNotificationsPermission(context: Context) {
|
fun requestNotificationsPermission() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val permission = ContextCompat.checkSelfPermission(
|
val permission = ContextCompat.checkSelfPermission(
|
||||||
context,
|
FlClashApplication.getAppContext(),
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
)
|
)
|
||||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||||
if (isBlockNotification) return
|
if (isBlockNotification) return
|
||||||
if (activity == null) return
|
if (activityRef?.get() == null) return
|
||||||
ActivityCompat.requestPermissions(
|
activityRef?.get()?.let {
|
||||||
activity!!,
|
ActivityCompat.requestPermissions(
|
||||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
it,
|
||||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
)
|
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||||
return
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getText(text: String): String? {
|
suspend fun getText(text: String): String? {
|
||||||
return runBlocking {
|
return withContext(Dispatchers.Default){
|
||||||
channel.awaitResult<String>("getText", text)
|
channel.awaitResult<String>("getText", text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isChinaPackage(packageName: String): Boolean {
|
private fun isChinaPackage(packageName: String): Boolean {
|
||||||
val packageManager = context.packageManager ?: return false
|
val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
|
||||||
skipPrefixList.forEach {
|
skipPrefixList.forEach {
|
||||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||||
}
|
}
|
||||||
@@ -373,7 +379,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION") packageManager.getPackageInfo(
|
packageManager.getPackageInfo(
|
||||||
packageName, packageManagerFlags
|
packageName, packageManagerFlags
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -420,28 +426,28 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activityRef = WeakReference(binding.activity)
|
||||||
binding.addActivityResultListener(::onActivityResult)
|
binding.addActivityResultListener(::onActivityResult)
|
||||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivityForConfigChanges() {
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
activity = null
|
activityRef = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activityRef = WeakReference(binding.activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {
|
override fun onDetachedFromActivity() {
|
||||||
channel.invokeMethod("exit", null)
|
channel.invokeMethod("exit", null)
|
||||||
activity = null
|
activityRef = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||||
GlobalState.initServiceEngine(context)
|
GlobalState.initServiceEngine()
|
||||||
vpnCallBack?.invoke()
|
vpnCallBack?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package com.follow.clash.plugins
|
package com.follow.clash.plugins
|
||||||
|
|
||||||
import android.content.Context
|
import com.follow.clash.FlClashApplication
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
|
import com.follow.clash.models.VpnOptions
|
||||||
|
import com.google.gson.Gson
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
|
|
||||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
private lateinit var flutterMethodChannel: MethodChannel
|
private lateinit var flutterMethodChannel: MethodChannel
|
||||||
|
|
||||||
private lateinit var context: Context
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
context = flutterPluginBinding.applicationContext
|
|
||||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
||||||
flutterMethodChannel.setMethodCallHandler(this)
|
flutterMethodChannel.setMethodCallHandler(this)
|
||||||
}
|
}
|
||||||
@@ -24,9 +23,22 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||||
|
"startVpn" -> {
|
||||||
|
val data = call.argument<String>("data")
|
||||||
|
val options = Gson().fromJson(data, VpnOptions::class.java)
|
||||||
|
GlobalState.getCurrentVPNPlugin()?.handleStart(options)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
"stopVpn" -> {
|
||||||
|
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
|
||||||
"init" -> {
|
"init" -> {
|
||||||
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
|
GlobalState.getCurrentAppPlugin()
|
||||||
GlobalState.initServiceEngine(context)
|
?.requestNotificationsPermission()
|
||||||
|
GlobalState.initServiceEngine()
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +53,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDestroy() {
|
private fun handleDestroy() {
|
||||||
GlobalState.getCurrentVPNPlugin()?.stop()
|
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||||
GlobalState.destroyServiceEngine()
|
GlobalState.destroyServiceEngine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
|
|
||||||
package com.follow.clash.plugins
|
package com.follow.clash.plugins
|
||||||
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin,
|
class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
MethodChannel.MethodCallHandler {
|
|
||||||
|
|
||||||
private lateinit var channel: MethodChannel
|
private lateinit var channel: MethodChannel
|
||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
|
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
|
||||||
channel.setMethodCallHandler(this)
|
channel.setMethodCallHandler(this)
|
||||||
@@ -20,13 +19,11 @@ class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleStart() {
|
fun handleStart() {
|
||||||
onStart?.let { it() }
|
|
||||||
channel.invokeMethod("start", null)
|
channel.invokeMethod("start", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleStop() {
|
fun handleStop() {
|
||||||
channel.invokeMethod("stop", null)
|
channel.invokeMethod("stop", null)
|
||||||
onStop?.let { it() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDetached() {
|
private fun handleDetached() {
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ import android.net.NetworkCapabilities
|
|||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import com.follow.clash.BaseServiceInterface
|
import com.follow.clash.FlClashApplication
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.RunState
|
import com.follow.clash.RunState
|
||||||
|
import com.follow.clash.extensions.awaitResult
|
||||||
import com.follow.clash.extensions.getProtocol
|
import com.follow.clash.extensions.getProtocol
|
||||||
import com.follow.clash.extensions.resolveDns
|
import com.follow.clash.extensions.resolveDns
|
||||||
import com.follow.clash.models.Process
|
import com.follow.clash.models.Process
|
||||||
|
import com.follow.clash.models.StartForegroundParams
|
||||||
import com.follow.clash.models.VpnOptions
|
import com.follow.clash.models.VpnOptions
|
||||||
|
import com.follow.clash.services.BaseServiceInterface
|
||||||
import com.follow.clash.services.FlClashService
|
import com.follow.clash.services.FlClashService
|
||||||
import com.follow.clash.services.FlClashVpnService
|
import com.follow.clash.services.FlClashVpnService
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
@@ -26,21 +30,24 @@ import io.flutter.plugin.common.MethodCall
|
|||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|
||||||
private lateinit var flutterMethodChannel: MethodChannel
|
private lateinit var flutterMethodChannel: MethodChannel
|
||||||
private lateinit var context: Context
|
|
||||||
private var flClashService: BaseServiceInterface? = null
|
private var flClashService: BaseServiceInterface? = null
|
||||||
private lateinit var options: VpnOptions
|
private lateinit var options: VpnOptions
|
||||||
private lateinit var scope: CoroutineScope
|
private lateinit var scope: CoroutineScope
|
||||||
|
private var lastStartForegroundParams: StartForegroundParams? = null
|
||||||
|
private var timerJob: Job? = null
|
||||||
|
|
||||||
private val connectivity by lazy {
|
private val connectivity by lazy {
|
||||||
context.getSystemService<ConnectivityManager>()
|
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connection = object : ServiceConnection {
|
private val connection = object : ServiceConnection {
|
||||||
@@ -50,7 +57,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
is FlClashService.LocalBinder -> service.getService()
|
is FlClashService.LocalBinder -> service.getService()
|
||||||
else -> throw Exception("invalid binder")
|
else -> throw Exception("invalid binder")
|
||||||
}
|
}
|
||||||
start()
|
handleStartService()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(arg: ComponentName) {
|
override fun onServiceDisconnected(arg: ComponentName) {
|
||||||
@@ -60,7 +67,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
scope = CoroutineScope(Dispatchers.Default)
|
scope = CoroutineScope(Dispatchers.Default)
|
||||||
context = flutterPluginBinding.applicationContext
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
registerNetworkCallback()
|
registerNetworkCallback()
|
||||||
}
|
}
|
||||||
@@ -77,38 +83,28 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
when (call.method) {
|
when (call.method) {
|
||||||
"start" -> {
|
"start" -> {
|
||||||
val data = call.argument<String>("data")
|
val data = call.argument<String>("data")
|
||||||
options = Gson().fromJson(data, VpnOptions::class.java)
|
result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
|
||||||
when (options.enable) {
|
|
||||||
true -> handleStartVpn()
|
|
||||||
false -> start()
|
|
||||||
}
|
|
||||||
result.success(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"stop" -> {
|
"stop" -> {
|
||||||
stop()
|
handleStop()
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
"setProtect" -> {
|
"setProtect" -> {
|
||||||
val fd = call.argument<Int>("fd")
|
val fd = call.argument<Int>("fd")
|
||||||
if (fd != null) {
|
if (fd != null && flClashService is FlClashVpnService) {
|
||||||
if (flClashService is FlClashVpnService) {
|
try {
|
||||||
(flClashService as FlClashVpnService).protect(fd)
|
(flClashService as FlClashVpnService).protect(fd)
|
||||||
|
result.success(true)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
result.success(false)
|
||||||
}
|
}
|
||||||
result.success(true)
|
|
||||||
} else {
|
} else {
|
||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"startForeground" -> {
|
|
||||||
val title = call.argument<String>("title") as String
|
|
||||||
val content = call.argument<String>("content") as String
|
|
||||||
startForeground(title, content)
|
|
||||||
result.success(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
"resolverProcess" -> {
|
"resolverProcess" -> {
|
||||||
val data = call.argument<String>("data")
|
val data = call.argument<String>("data")
|
||||||
val process = if (data != null) Gson().fromJson(
|
val process = if (data != null) Gson().fromJson(
|
||||||
@@ -144,7 +140,8 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
result.success(null)
|
result.success(null)
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
val packages =
|
||||||
|
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
|
||||||
result.success(packages?.first())
|
result.success(packages?.first())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,10 +153,20 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleStartVpn() {
|
fun handleStart(options: VpnOptions): Boolean {
|
||||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
this.options = options
|
||||||
start()
|
when (options.enable) {
|
||||||
|
true -> handleStartVpn()
|
||||||
|
false -> handleStartService()
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStartVpn() {
|
||||||
|
GlobalState.getCurrentAppPlugin()
|
||||||
|
?.requestVpnPermission {
|
||||||
|
handleStartService()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestGc() {
|
fun requestGc() {
|
||||||
@@ -177,16 +184,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if (flClashService is FlClashVpnService) {
|
|
||||||
// val network = networks.maxByOrNull { net ->
|
|
||||||
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
|
|
||||||
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
|
|
||||||
// } ?: -1
|
|
||||||
// }
|
|
||||||
// network?.let {
|
|
||||||
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = object : ConnectivityManager.NetworkCallback() {
|
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
@@ -218,14 +215,41 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
onUpdateNetwork()
|
onUpdateNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startForeground(title: String, content: String) {
|
private suspend fun startForeground() {
|
||||||
GlobalState.runLock.withLock {
|
GlobalState.runLock.lock()
|
||||||
|
try {
|
||||||
if (GlobalState.runState.value != RunState.START) return
|
if (GlobalState.runState.value != RunState.START) return
|
||||||
flClashService?.startForeground(title, content)
|
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
|
||||||
|
val startForegroundParams = Gson().fromJson(
|
||||||
|
data, StartForegroundParams::class.java
|
||||||
|
)
|
||||||
|
if (lastStartForegroundParams != startForegroundParams) {
|
||||||
|
lastStartForegroundParams = startForegroundParams
|
||||||
|
flClashService?.startForeground(
|
||||||
|
startForegroundParams.title,
|
||||||
|
startForegroundParams.content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
GlobalState.runLock.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun start() {
|
private fun startForegroundJob() {
|
||||||
|
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
while (isActive) {
|
||||||
|
startForeground()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopForegroundJob() {
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStartService() {
|
||||||
if (flClashService == null) {
|
if (flClashService == null) {
|
||||||
bindService()
|
bindService()
|
||||||
return
|
return
|
||||||
@@ -237,24 +261,25 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
flutterMethodChannel.invokeMethod(
|
flutterMethodChannel.invokeMethod(
|
||||||
"started", fd
|
"started", fd
|
||||||
)
|
)
|
||||||
|
startForegroundJob();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun handleStop() {
|
||||||
GlobalState.runLock.withLock {
|
GlobalState.runLock.withLock {
|
||||||
if (GlobalState.runState.value == RunState.STOP) return
|
if (GlobalState.runState.value == RunState.STOP) return
|
||||||
GlobalState.runState.value = RunState.STOP
|
GlobalState.runState.value = RunState.STOP
|
||||||
|
stopForegroundJob()
|
||||||
flClashService?.stop()
|
flClashService?.stop()
|
||||||
|
GlobalState.handleTryDestroy()
|
||||||
}
|
}
|
||||||
GlobalState.destroyServiceEngine()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
val intent = when (options.enable) {
|
val intent = when (options.enable) {
|
||||||
true -> Intent(context, FlClashVpnService::class.java)
|
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
|
||||||
false -> Intent(context, FlClashService::class.java)
|
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
|
||||||
}
|
}
|
||||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.follow.clash
|
package com.follow.clash.services
|
||||||
|
|
||||||
|
|
||||||
import com.follow.clash.models.VpnOptions
|
import com.follow.clash.models.VpnOptions
|
||||||
|
|
||||||
interface BaseServiceInterface {
|
interface BaseServiceInterface {
|
||||||
|
|
||||||
fun start(options: VpnOptions): Int
|
fun start(options: VpnOptions): Int
|
||||||
|
|
||||||
fun stop()
|
fun stop()
|
||||||
fun startForeground(title: String, content: String)
|
|
||||||
|
suspend fun startForeground(title: String, content: String)
|
||||||
}
|
}
|
||||||
@@ -12,14 +12,14 @@ import android.os.Binder
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.follow.clash.BaseServiceInterface
|
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.MainActivity
|
import com.follow.clash.MainActivity
|
||||||
import com.follow.clash.extensions.getActionPendingIntent
|
import com.follow.clash.extensions.getActionPendingIntent
|
||||||
import com.follow.clash.models.VpnOptions
|
import com.follow.clash.models.VpnOptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
|
|
||||||
class FlClashService : Service(), BaseServiceInterface {
|
class FlClashService : Service(), BaseServiceInterface {
|
||||||
@@ -42,44 +42,54 @@ class FlClashService : Service(), BaseServiceInterface {
|
|||||||
|
|
||||||
private val notificationId: Int = 1
|
private val notificationId: Int = 1
|
||||||
|
|
||||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
CoroutineScope(Dispatchers.Main).async {
|
||||||
|
val stopText = GlobalState.getText("stop")
|
||||||
|
|
||||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
val intent = Intent(
|
||||||
PendingIntent.getActivity(
|
this@FlClashService, MainActivity::class.java
|
||||||
this,
|
|
||||||
0,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
PendingIntent.getActivity(
|
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||||
this,
|
PendingIntent.getActivity(
|
||||||
0,
|
this@FlClashService,
|
||||||
intent,
|
0,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
intent,
|
||||||
)
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
}
|
)
|
||||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
} else {
|
||||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
PendingIntent.getActivity(
|
||||||
setContentTitle("FlClash")
|
this@FlClashService,
|
||||||
setContentIntent(pendingIntent)
|
0,
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
intent,
|
||||||
priority = NotificationCompat.PRIORITY_MIN
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
)
|
||||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
}
|
||||||
|
|
||||||
|
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
|
||||||
|
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||||
|
setContentTitle("FlClash")
|
||||||
|
setContentIntent(pendingIntent)
|
||||||
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
priority = NotificationCompat.PRIORITY_MIN
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||||
|
}
|
||||||
|
addAction(
|
||||||
|
0,
|
||||||
|
stopText, // 使用 suspend 函数获取的文本
|
||||||
|
getActionPendingIntent("STOP")
|
||||||
|
)
|
||||||
|
setOngoing(true)
|
||||||
|
setShowWhen(false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setAutoCancel(true)
|
||||||
}
|
}
|
||||||
addAction(
|
|
||||||
0,
|
|
||||||
GlobalState.getText("stop"),
|
|
||||||
getActionPendingIntent("STOP")
|
|
||||||
)
|
|
||||||
setOngoing(true)
|
|
||||||
setShowWhen(false)
|
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
setAutoCancel(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||||
|
return notificationBuilderDeferred.await()
|
||||||
|
}
|
||||||
|
|
||||||
override fun start(options: VpnOptions) = 0
|
override fun start(options: VpnOptions) = 0
|
||||||
|
|
||||||
@@ -91,24 +101,24 @@ class FlClashService : Service(), BaseServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||||
override fun startForeground(title: String, content: String) {
|
override suspend fun startForeground(title: String, content: String) {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
if (channel == null) {
|
||||||
if (channel == null) {
|
channel =
|
||||||
channel =
|
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
manager?.createNotificationChannel(channel)
|
||||||
manager?.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val notification =
|
|
||||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
|
||||||
} else {
|
|
||||||
startForeground(notificationId, notification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val notification =
|
||||||
|
getNotificationBuilder()
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content).build()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
} else {
|
||||||
|
startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ class FlClashTileService : TileService() {
|
|||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
activityTransfer()
|
activityTransfer()
|
||||||
GlobalState.handleToggle(applicationContext)
|
GlobalState.handleToggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
import android.net.Network
|
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
@@ -17,7 +16,6 @@ import android.os.Parcel
|
|||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.follow.clash.BaseServiceInterface
|
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.MainActivity
|
import com.follow.clash.MainActivity
|
||||||
import com.follow.clash.R
|
import com.follow.clash.R
|
||||||
@@ -28,14 +26,16 @@ import com.follow.clash.extensions.toCIDR
|
|||||||
import com.follow.clash.models.AccessControlMode
|
import com.follow.clash.models.AccessControlMode
|
||||||
import com.follow.clash.models.VpnOptions
|
import com.follow.clash.models.VpnOptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
GlobalState.initServiceEngine(applicationContext)
|
GlobalState.initServiceEngine()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start(options: VpnOptions): Int {
|
override fun start(options: VpnOptions): Int {
|
||||||
@@ -68,17 +68,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
}
|
}
|
||||||
addDnsServer(options.dnsServerAddress)
|
addDnsServer(options.dnsServerAddress)
|
||||||
setMtu(9000)
|
setMtu(9000)
|
||||||
options.accessControl?.let { accessControl ->
|
options.accessControl.let { accessControl ->
|
||||||
when (accessControl.mode) {
|
if (accessControl.enable) {
|
||||||
AccessControlMode.acceptSelected -> {
|
when (accessControl.mode) {
|
||||||
(accessControl.acceptList + packageName).forEach {
|
AccessControlMode.acceptSelected -> {
|
||||||
addAllowedApplication(it)
|
(accessControl.acceptList + packageName).forEach {
|
||||||
|
addAllowedApplication(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
AccessControlMode.rejectSelected -> {
|
AccessControlMode.rejectSelected -> {
|
||||||
(accessControl.rejectList - packageName).forEach {
|
(accessControl.rejectList - packageName).forEach {
|
||||||
addDisallowedApplication(it)
|
addDisallowedApplication(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,12 +107,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateUnderlyingNetworks(networks: Array<Network>) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
this.setUnderlyingNetworks(networks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
@@ -122,69 +118,74 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
|
|
||||||
private val notificationId: Int = 1
|
private val notificationId: Int = 1
|
||||||
|
|
||||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
CoroutineScope(Dispatchers.Main).async {
|
||||||
|
val stopText = GlobalState.getText("stop")
|
||||||
|
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
|
||||||
|
|
||||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this@FlClashVpnService,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this@FlClashVpnService,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
|
||||||
setSmallIcon(R.drawable.ic_stat_name)
|
setSmallIcon(R.drawable.ic_stat_name)
|
||||||
setContentTitle("FlClash")
|
setContentTitle("FlClash")
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
priority = NotificationCompat.PRIORITY_MIN
|
priority = NotificationCompat.PRIORITY_MIN
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||||
|
}
|
||||||
|
setOngoing(true)
|
||||||
|
addAction(
|
||||||
|
0,
|
||||||
|
stopText,
|
||||||
|
getActionPendingIntent("STOP")
|
||||||
|
)
|
||||||
|
setShowWhen(false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setAutoCancel(true)
|
||||||
}
|
}
|
||||||
setOngoing(true)
|
|
||||||
addAction(
|
|
||||||
0,
|
|
||||||
GlobalState.getText("stop"),
|
|
||||||
getActionPendingIntent("STOP")
|
|
||||||
)
|
|
||||||
setShowWhen(false)
|
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
setAutoCancel(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||||
|
return notificationBuilderDeferred.await()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||||
override fun startForeground(title: String, content: String) {
|
override suspend fun startForeground(title: String, content: String) {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
if (channel == null) {
|
||||||
if (channel == null) {
|
channel =
|
||||||
channel =
|
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
manager?.createNotificationChannel(channel)
|
||||||
manager?.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val notification =
|
|
||||||
notificationBuilder
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(content)
|
|
||||||
.build()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
|
||||||
} else {
|
|
||||||
startForeground(notificationId, notification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val notification =
|
||||||
|
getNotificationBuilder()
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.build()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
} else {
|
||||||
|
startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ targets:
|
|||||||
options:
|
options:
|
||||||
build_extensions:
|
build_extensions:
|
||||||
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
||||||
|
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
|
||||||
freezed:
|
freezed:
|
||||||
options:
|
options:
|
||||||
build_extensions:
|
build_extensions:
|
||||||
|
|||||||
Submodule core/Clash.Meta updated: 3175efe8c0...76b0d7e8bc
181
core/action.go
181
core/action.go
@@ -1,32 +1,177 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (action Action) Json() ([]byte, error) {
|
type Action struct {
|
||||||
data, err := json.Marshal(action)
|
Id string `json:"id"`
|
||||||
|
Method Method `json:"method"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
DefaultValue interface{} `json:"default-value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionResult struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Method Method `json:"method"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (result ActionResult) Json() ([]byte, error) {
|
||||||
|
data, err := json.Marshal(result)
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (action Action) callback(data interface{}) bool {
|
func (action Action) getResult(data interface{}) []byte {
|
||||||
if conn == nil {
|
resultAction := ActionResult{
|
||||||
return false
|
|
||||||
}
|
|
||||||
sendAction := Action{
|
|
||||||
Id: action.Id,
|
Id: action.Id,
|
||||||
Method: action.Method,
|
Method: action.Method,
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
res, err := sendAction.Json()
|
res, _ := resultAction.Json()
|
||||||
if err != nil {
|
return res
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
_, err = conn.Write(append(res, []byte("\n")...))
|
func handleAction(action *Action, result func(data interface{})) {
|
||||||
if err != nil {
|
switch action.Method {
|
||||||
return false
|
case initClashMethod:
|
||||||
}
|
data := action.Data.(string)
|
||||||
return true
|
result(handleInitClash(data))
|
||||||
|
return
|
||||||
|
case getIsInitMethod:
|
||||||
|
result(handleGetIsInit())
|
||||||
|
return
|
||||||
|
case forceGcMethod:
|
||||||
|
handleForceGc()
|
||||||
|
result(true)
|
||||||
|
return
|
||||||
|
case shutdownMethod:
|
||||||
|
result(handleShutdown())
|
||||||
|
return
|
||||||
|
case validateConfigMethod:
|
||||||
|
data := []byte(action.Data.(string))
|
||||||
|
result(handleValidateConfig(data))
|
||||||
|
return
|
||||||
|
case updateConfigMethod:
|
||||||
|
data := []byte(action.Data.(string))
|
||||||
|
result(handleUpdateConfig(data))
|
||||||
|
return
|
||||||
|
case getProxiesMethod:
|
||||||
|
result(handleGetProxies())
|
||||||
|
return
|
||||||
|
case changeProxyMethod:
|
||||||
|
data := action.Data.(string)
|
||||||
|
handleChangeProxy(data, func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case getTrafficMethod:
|
||||||
|
result(handleGetTraffic())
|
||||||
|
return
|
||||||
|
case getTotalTrafficMethod:
|
||||||
|
result(handleGetTotalTraffic())
|
||||||
|
return
|
||||||
|
case resetTrafficMethod:
|
||||||
|
handleResetTraffic()
|
||||||
|
result(true)
|
||||||
|
return
|
||||||
|
case asyncTestDelayMethod:
|
||||||
|
data := action.Data.(string)
|
||||||
|
handleAsyncTestDelay(data, func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case getConnectionsMethod:
|
||||||
|
result(handleGetConnections())
|
||||||
|
return
|
||||||
|
case closeConnectionsMethod:
|
||||||
|
result(handleCloseConnections())
|
||||||
|
return
|
||||||
|
case closeConnectionMethod:
|
||||||
|
id := action.Data.(string)
|
||||||
|
result(handleCloseConnection(id))
|
||||||
|
return
|
||||||
|
case getExternalProvidersMethod:
|
||||||
|
result(handleGetExternalProviders())
|
||||||
|
return
|
||||||
|
case getExternalProviderMethod:
|
||||||
|
externalProviderName := action.Data.(string)
|
||||||
|
result(handleGetExternalProvider(externalProviderName))
|
||||||
|
case updateGeoDataMethod:
|
||||||
|
paramsString := action.Data.(string)
|
||||||
|
var params = map[string]string{}
|
||||||
|
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
result(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
geoType := params["geo-type"]
|
||||||
|
geoName := params["geo-name"]
|
||||||
|
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case updateExternalProviderMethod:
|
||||||
|
providerName := action.Data.(string)
|
||||||
|
handleUpdateExternalProvider(providerName, func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case sideLoadExternalProviderMethod:
|
||||||
|
paramsString := action.Data.(string)
|
||||||
|
var params = map[string]string{}
|
||||||
|
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
result(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
providerName := params["providerName"]
|
||||||
|
data := params["data"]
|
||||||
|
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case startLogMethod:
|
||||||
|
handleStartLog()
|
||||||
|
result(true)
|
||||||
|
return
|
||||||
|
case stopLogMethod:
|
||||||
|
handleStopLog()
|
||||||
|
result(true)
|
||||||
|
return
|
||||||
|
case startListenerMethod:
|
||||||
|
result(handleStartListener())
|
||||||
|
return
|
||||||
|
case stopListenerMethod:
|
||||||
|
result(handleStopListener())
|
||||||
|
return
|
||||||
|
case getCountryCodeMethod:
|
||||||
|
ip := action.Data.(string)
|
||||||
|
handleGetCountryCode(ip, func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case getMemoryMethod:
|
||||||
|
handleGetMemory(func(value string) {
|
||||||
|
result(value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case getProfileMethod:
|
||||||
|
profileId := action.Data.(string)
|
||||||
|
handleGetMemory(func(value string) {
|
||||||
|
result(handleGetProfile(profileId))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case setStateMethod:
|
||||||
|
data := action.Data.(string)
|
||||||
|
handleSetState(data)
|
||||||
|
result(true)
|
||||||
|
default:
|
||||||
|
handle := nextHandle(action, result)
|
||||||
|
if handle {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
result(action.DefaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/metacubex/mihomo/adapter"
|
"github.com/metacubex/mihomo/adapter"
|
||||||
@@ -32,7 +31,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
isRunning = false
|
isRunning = false
|
||||||
runLock sync.Mutex
|
runLock sync.Mutex
|
||||||
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
|
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,11 +41,6 @@ func (a ExternalProviders) Len() int { return len(a) }
|
|||||||
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||||
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
func (message *Message) Json() (string, error) {
|
|
||||||
data, err := json.Marshal(message)
|
|
||||||
return string(data), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func readFile(path string) ([]byte, error) {
|
func readFile(path string) ([]byte, error) {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -85,16 +79,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||||
if configParams.TestURL != nil {
|
|
||||||
if mapping["health-check"] != nil {
|
|
||||||
hc := mapping["health-check"].(map[string]any)
|
|
||||||
if hc != nil {
|
|
||||||
if hc["url"] != nil {
|
|
||||||
hc["url"] = *configParams.TestURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, mapping := range prof.RuleProvider {
|
for _, mapping := range prof.RuleProvider {
|
||||||
value, exist := mapping["path"].(string)
|
value, exist := mapping["path"].(string)
|
||||||
@@ -231,6 +215,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
|||||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||||
|
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||||
targetConfig.Profile.StoreSelected = false
|
targetConfig.Profile.StoreSelected = false
|
||||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/metacubex/mihomo/adapter/provider"
|
"github.com/metacubex/mihomo/adapter/provider"
|
||||||
"github.com/metacubex/mihomo/config"
|
"github.com/metacubex/mihomo/config"
|
||||||
"github.com/metacubex/mihomo/constant"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,14 +28,10 @@ type ChangeProxyParams struct {
|
|||||||
|
|
||||||
type TestDelayParams struct {
|
type TestDelayParams struct {
|
||||||
ProxyName string `json:"proxy-name"`
|
ProxyName string `json:"proxy-name"`
|
||||||
|
TestUrl string `json:"test-url"`
|
||||||
Timeout int64 `json:"timeout"`
|
Timeout int64 `json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProcessMapItem struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExternalProvider struct {
|
type ExternalProvider struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -74,19 +70,24 @@ const (
|
|||||||
stopLogMethod Method = "stopLog"
|
stopLogMethod Method = "stopLog"
|
||||||
startListenerMethod Method = "startListener"
|
startListenerMethod Method = "startListener"
|
||||||
stopListenerMethod Method = "stopListener"
|
stopListenerMethod Method = "stopListener"
|
||||||
|
startTunMethod Method = "startTun"
|
||||||
|
stopTunMethod Method = "stopTun"
|
||||||
|
updateDnsMethod Method = "updateDns"
|
||||||
|
setProcessMapMethod Method = "setProcessMap"
|
||||||
|
setFdMapMethod Method = "setFdMap"
|
||||||
|
setStateMethod Method = "setState"
|
||||||
|
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||||
|
getRunTimeMethod Method = "getRunTime"
|
||||||
|
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||||
|
getProfileMethod Method = "getProfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Method string
|
type Method string
|
||||||
|
|
||||||
type Action struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Method Method `json:"method"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageType string
|
type MessageType string
|
||||||
|
|
||||||
type Delay struct {
|
type Delay struct {
|
||||||
|
Url string `json:"url"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value int32 `json:"value"`
|
Value int32 `json:"value"`
|
||||||
}
|
}
|
||||||
@@ -96,17 +97,31 @@ type Message struct {
|
|||||||
Data interface{} `json:"data"`
|
Data interface{} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Process struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Metadata *constant.Metadata `json:"metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogMessage MessageType = "log"
|
LogMessage MessageType = "log"
|
||||||
ProtectMessage MessageType = "protect"
|
|
||||||
DelayMessage MessageType = "delay"
|
DelayMessage MessageType = "delay"
|
||||||
ProcessMessage MessageType = "process"
|
|
||||||
RequestMessage MessageType = "request"
|
RequestMessage MessageType = "request"
|
||||||
StartedMessage MessageType = "started"
|
|
||||||
LoadedMessage MessageType = "loaded"
|
LoadedMessage MessageType = "loaded"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (message *Message) Json() (string, error) {
|
||||||
|
data, err := json.Marshal(message)
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvokeMessage struct {
|
||||||
|
Type InvokeType `json:"type"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvokeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProtectInvoke InvokeType = "protect"
|
||||||
|
ProcessInvoke InvokeType = "process"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (message *InvokeMessage) Json() string {
|
||||||
|
data, _ := json.Marshal(message)
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|||||||
41
core/go.mod
41
core/go.mod
@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||||
github.com/samber/lo v1.47.0
|
github.com/samber/lo v1.49.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -19,28 +19,28 @@ require (
|
|||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
github.com/cloudflare/circl v1.3.7 // indirect
|
||||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/ebitengine/purego v0.8.1 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
github.com/enfein/mieru/v3 v3.10.0 // indirect
|
github.com/enfein/mieru/v3 v3.11.2 // indirect
|
||||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||||
github.com/go-chi/render v1.0.3 // indirect
|
github.com/go-chi/render v1.0.3 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
github.com/gofrs/uuid/v5 v5.3.0 // indirect
|
github.com/gofrs/uuid/v5 v5.3.1 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
@@ -51,21 +51,22 @@ require (
|
|||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||||
github.com/metacubex/chacha v0.1.0 // indirect
|
github.com/metacubex/chacha v0.1.1 // indirect
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
|
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
|
||||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
|
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
|
||||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
|
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
|
||||||
|
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
|
||||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||||
github.com/metacubex/sing-tun v0.4.5 // indirect
|
github.com/metacubex/sing-tun v0.4.5 // indirect
|
||||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||||
github.com/metacubex/utls v1.6.6 // indirect
|
github.com/metacubex/utls v1.6.6 // indirect
|
||||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.63 // indirect
|
||||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
@@ -73,18 +74,18 @@ require (
|
|||||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||||
github.com/sagernet/cors v1.2.1 // indirect
|
github.com/sagernet/cors v1.2.1 // indirect
|
||||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||||
github.com/sagernet/sing v0.5.1 // indirect
|
github.com/sagernet/sing v0.5.2 // indirect
|
||||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||||
@@ -101,13 +102,13 @@ require (
|
|||||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
go.uber.org/mock v0.4.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||||
golang.org/x/mod v0.20.0 // indirect
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/time v0.7.0 // indirect
|
golang.org/x/time v0.7.0 // indirect
|
||||||
golang.org/x/tools v0.24.0 // indirect
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
|||||||
88
core/go.sum
88
core/go.sum
@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
|
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
|
||||||
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
|
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
|
||||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||||
@@ -43,8 +43,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
|||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||||
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
|||||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
@@ -84,6 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
|
|||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||||
@@ -98,26 +99,28 @@ github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31
|
|||||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||||
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
|
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
|
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
|
||||||
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
|
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
|
||||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
|
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
|
||||||
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
|
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
|
||||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
|
||||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
|
||||||
|
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
|
||||||
|
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||||
@@ -126,10 +129,11 @@ github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
|
|||||||
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
|
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||||
@@ -149,8 +153,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||||
@@ -164,18 +168,18 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
|
|||||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
|
||||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||||
@@ -218,8 +222,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
|||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -228,11 +232,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
|||||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -248,12 +252,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
@@ -263,8 +267,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
53
core/hub.go
53
core/hub.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"core/state"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/metacubex/mihomo/adapter"
|
"github.com/metacubex/mihomo/adapter"
|
||||||
@@ -149,8 +150,8 @@ func handleChangeProxy(data string, fn func(string string)) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetTraffic(onlyProxy bool) string {
|
func handleGetTraffic() string {
|
||||||
up, down := statistic.DefaultManager.Current(onlyProxy)
|
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
|
||||||
traffic := map[string]int64{
|
traffic := map[string]int64{
|
||||||
"up": up,
|
"up": up,
|
||||||
"down": down,
|
"down": down,
|
||||||
@@ -163,8 +164,8 @@ func handleGetTraffic(onlyProxy bool) string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetTotalTraffic(onlyProxy bool) string {
|
func handleGetTotalTraffic() string {
|
||||||
up, down := statistic.DefaultManager.Total(onlyProxy)
|
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
|
||||||
traffic := map[string]int64{
|
traffic := map[string]int64{
|
||||||
"up": up,
|
"up": up,
|
||||||
"down": down,
|
"down": down,
|
||||||
@@ -177,6 +178,15 @@ func handleGetTotalTraffic(onlyProxy bool) string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleGetProfile(profileId string) string {
|
||||||
|
prof := getRawConfigWithId(profileId)
|
||||||
|
data, err := json.Marshal(prof)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
func handleResetTraffic() {
|
func handleResetTraffic() {
|
||||||
statistic.DefaultManager.ResetStatistic()
|
statistic.DefaultManager.ResetStatistic()
|
||||||
}
|
}
|
||||||
@@ -213,7 +223,14 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
testUrl := constant.DefaultTestURL
|
||||||
|
|
||||||
|
if params.TestUrl != "" {
|
||||||
|
testUrl = params.TestUrl
|
||||||
|
}
|
||||||
|
delayData.Url = testUrl
|
||||||
|
|
||||||
|
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||||
if err != nil || delay == 0 {
|
if err != nil || delay == 0 {
|
||||||
delayData.Value = -1
|
delayData.Value = -1
|
||||||
data, _ := json.Marshal(delayData)
|
data, _ := json.Marshal(delayData)
|
||||||
@@ -240,17 +257,6 @@ func handleGetConnections() string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseConnectionsUnLock() bool {
|
|
||||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
|
||||||
err := c.Close()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCloseConnections() bool {
|
func handleCloseConnections() bool {
|
||||||
runLock.Lock()
|
runLock.Lock()
|
||||||
defer runLock.Unlock()
|
defer runLock.Unlock()
|
||||||
@@ -395,7 +401,7 @@ func handleStartLog() {
|
|||||||
Type: LogMessage,
|
Type: LogMessage,
|
||||||
Data: logData,
|
Data: logData,
|
||||||
}
|
}
|
||||||
SendMessage(*message)
|
sendMessage(*message)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -426,9 +432,14 @@ func handleGetMemory(fn func(value string)) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSetState(params string) {
|
||||||
|
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
adapter.UrlTestHook = func(name string, delay uint16) {
|
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||||
delayData := &Delay{
|
delayData := &Delay{
|
||||||
|
Url: url,
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
if delay == 0 {
|
if delay == 0 {
|
||||||
@@ -436,19 +447,19 @@ func init() {
|
|||||||
} else {
|
} else {
|
||||||
delayData.Value = int32(delay)
|
delayData.Value = int32(delay)
|
||||||
}
|
}
|
||||||
SendMessage(Message{
|
sendMessage(Message{
|
||||||
Type: DelayMessage,
|
Type: DelayMessage,
|
||||||
Data: delayData,
|
Data: delayData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
||||||
SendMessage(Message{
|
sendMessage(Message{
|
||||||
Type: RequestMessage,
|
Type: RequestMessage,
|
||||||
Data: c,
|
Data: c,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
executor.DefaultProviderLoadedHook = func(providerName string) {
|
executor.DefaultProviderLoadedHook = func(providerName string) {
|
||||||
SendMessage(Message{
|
sendMessage(Message{
|
||||||
Type: LoadedMessage,
|
Type: LoadedMessage,
|
||||||
Data: providerName,
|
Data: providerName,
|
||||||
})
|
})
|
||||||
|
|||||||
205
core/lib.go
205
core/lib.go
@@ -8,18 +8,30 @@ package main
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
bridge "core/dart-bridge"
|
bridge "core/dart-bridge"
|
||||||
|
"encoding/json"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var messagePort int64 = -1
|
||||||
|
|
||||||
//export initNativeApiBridge
|
//export initNativeApiBridge
|
||||||
func initNativeApiBridge(api unsafe.Pointer) {
|
func initNativeApiBridge(api unsafe.Pointer) {
|
||||||
bridge.InitDartApi(api)
|
bridge.InitDartApi(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export initMessage
|
//export attachMessagePort
|
||||||
func initMessage(port C.longlong) {
|
func attachMessagePort(mPort C.longlong) {
|
||||||
i := int64(port)
|
messagePort = int64(mPort)
|
||||||
Port = i
|
}
|
||||||
|
|
||||||
|
//export getTraffic
|
||||||
|
func getTraffic() *C.char {
|
||||||
|
return C.CString(handleGetTraffic())
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getTotalTraffic
|
||||||
|
func getTotalTraffic() *C.char {
|
||||||
|
return C.CString(handleGetTotalTraffic())
|
||||||
}
|
}
|
||||||
|
|
||||||
//export freeCString
|
//export freeCString
|
||||||
@@ -27,9 +39,32 @@ func freeCString(s *C.char) {
|
|||||||
C.free(unsafe.Pointer(s))
|
C.free(unsafe.Pointer(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
//export initClash
|
//export invokeAction
|
||||||
func initClash(homeDirStr *C.char) bool {
|
func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||||
return handleInitClash(C.GoString(homeDirStr))
|
params := C.GoString(paramsChar)
|
||||||
|
i := int64(port)
|
||||||
|
var action = &Action{}
|
||||||
|
err := json.Unmarshal([]byte(params), action)
|
||||||
|
if err != nil {
|
||||||
|
bridge.SendToPort(i, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go handleAction(action, func(data interface{}) {
|
||||||
|
bridge.SendToPort(i, string(action.getResult(data)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessage(message Message) {
|
||||||
|
if messagePort == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := message.Json()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bridge.SendToPort(messagePort, string(Action{
|
||||||
|
Method: messageMethod,
|
||||||
|
}.getResult(res)))
|
||||||
}
|
}
|
||||||
|
|
||||||
//export startListener
|
//export startListener
|
||||||
@@ -41,159 +76,3 @@ func startListener() {
|
|||||||
func stopListener() {
|
func stopListener() {
|
||||||
handleStopListener()
|
handleStopListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
//export getIsInit
|
|
||||||
func getIsInit() bool {
|
|
||||||
return handleGetIsInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export shutdownClash
|
|
||||||
func shutdownClash() bool {
|
|
||||||
return handleShutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export forceGc
|
|
||||||
func forceGc() {
|
|
||||||
handleForceGc()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export validateConfig
|
|
||||||
func validateConfig(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
bytes := []byte(C.GoString(s))
|
|
||||||
go func() {
|
|
||||||
bridge.SendToPort(i, handleValidateConfig(bytes))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export updateConfig
|
|
||||||
func updateConfig(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
bytes := []byte(C.GoString(s))
|
|
||||||
go func() {
|
|
||||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getProxies
|
|
||||||
func getProxies() *C.char {
|
|
||||||
return C.CString(handleGetProxies())
|
|
||||||
}
|
|
||||||
|
|
||||||
//export changeProxy
|
|
||||||
func changeProxy(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
handleChangeProxy(paramsString, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getTraffic
|
|
||||||
func getTraffic(port C.int) *C.char {
|
|
||||||
onlyProxy := int(port) == 1
|
|
||||||
return C.CString(handleGetTraffic(onlyProxy))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getTotalTraffic
|
|
||||||
func getTotalTraffic(port C.int) *C.char {
|
|
||||||
onlyProxy := int(port) == 1
|
|
||||||
return C.CString(handleGetTotalTraffic(onlyProxy))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export resetTraffic
|
|
||||||
func resetTraffic() {
|
|
||||||
handleResetTraffic()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export asyncTestDelay
|
|
||||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
handleAsyncTestDelay(paramsString, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getConnections
|
|
||||||
func getConnections() *C.char {
|
|
||||||
return C.CString(handleGetConnections())
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getMemory
|
|
||||||
func getMemory(port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
handleGetMemory(func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export closeConnections
|
|
||||||
func closeConnections() {
|
|
||||||
handleCloseConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export closeConnection
|
|
||||||
func closeConnection(id *C.char) {
|
|
||||||
connectionId := C.GoString(id)
|
|
||||||
handleCloseConnection(connectionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getExternalProviders
|
|
||||||
func getExternalProviders() *C.char {
|
|
||||||
return C.CString(handleGetExternalProviders())
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getExternalProvider
|
|
||||||
func getExternalProvider(externalProviderNameChar *C.char) *C.char {
|
|
||||||
externalProviderName := C.GoString(externalProviderNameChar)
|
|
||||||
return C.CString(handleGetExternalProvider(externalProviderName))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export updateGeoData
|
|
||||||
func updateGeoData(geoTypeChar *C.char, geoNameChar *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
geoType := C.GoString(geoTypeChar)
|
|
||||||
geoName := C.GoString(geoNameChar)
|
|
||||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export updateExternalProvider
|
|
||||||
func updateExternalProvider(providerNameChar *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
providerName := C.GoString(providerNameChar)
|
|
||||||
handleUpdateExternalProvider(providerName, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getCountryCode
|
|
||||||
func getCountryCode(ipChar *C.char, port C.longlong) {
|
|
||||||
ip := C.GoString(ipChar)
|
|
||||||
i := int64(port)
|
|
||||||
handleGetCountryCode(ip, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export sideLoadExternalProvider
|
|
||||||
func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
providerName := C.GoString(providerNameChar)
|
|
||||||
data := []byte(C.GoString(dataChar))
|
|
||||||
handleSideLoadExternalProvider(providerName, data, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export startLog
|
|
||||||
func startLog() {
|
|
||||||
handleStartLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export stopLog
|
|
||||||
func stopLog() {
|
|
||||||
handleStopLog()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ package main
|
|||||||
|
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
|
bridge "core/dart-bridge"
|
||||||
"core/platform"
|
"core/platform"
|
||||||
"core/state"
|
"core/state"
|
||||||
t "core/tun"
|
t "core/tun"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/component/process"
|
"github.com/metacubex/mihomo/component/process"
|
||||||
"github.com/metacubex/mihomo/constant"
|
"github.com/metacubex/mihomo/constant"
|
||||||
@@ -19,123 +21,158 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProcessMap struct {
|
|
||||||
m sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
type FdMap struct {
|
|
||||||
m sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
type Fd struct {
|
type Fd struct {
|
||||||
Id int64 `json:"id"`
|
Id string `json:"id"`
|
||||||
Value int64 `json:"value"`
|
Value int64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Process struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Metadata *constant.Metadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessMapItem struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvokeManager struct {
|
||||||
|
invokeMap sync.Map
|
||||||
|
chanMap map[string]chan struct{}
|
||||||
|
chanLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInvokeManager() *InvokeManager {
|
||||||
|
return &InvokeManager{
|
||||||
|
chanMap: make(map[string]chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InvokeManager) 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) 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:
|
||||||
|
res, ok := m.invokeMap.Load(id)
|
||||||
|
m.invokeMap.Delete(id)
|
||||||
|
if ok {
|
||||||
|
return res.(string)
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
m.completer(id, "")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tunListener *sing_tun.Listener
|
invokePort int64 = -1
|
||||||
fdMap FdMap
|
tunListener *sing_tun.Listener
|
||||||
fdCounter int64 = 0
|
fdInvokeMap = NewInvokeManager()
|
||||||
counter int64 = 0
|
processInvokeMap = NewInvokeManager()
|
||||||
processMap ProcessMap
|
tunLock sync.Mutex
|
||||||
tunLock sync.Mutex
|
runTime *time.Time
|
||||||
runTime *time.Time
|
errBlocked = errors.New("blocked")
|
||||||
errBlocked = errors.New("blocked")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cm *ProcessMap) Store(key int64, value string) {
|
func handleStartTun(fd int) string {
|
||||||
cm.m.Store(key, value)
|
handleStopTun()
|
||||||
}
|
tunLock.Lock()
|
||||||
|
defer tunLock.Unlock()
|
||||||
func (cm *ProcessMap) Load(key int64) (string, bool) {
|
|
||||||
value, ok := cm.m.Load(key)
|
|
||||||
if !ok || value == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return value.(string), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *FdMap) Store(key int64) {
|
|
||||||
cm.m.Store(key, struct{}{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *FdMap) Load(key int64) bool {
|
|
||||||
_, ok := cm.m.Load(key)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
//export startTUN
|
|
||||||
func startTUN(fd C.int, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
ServicePort = i
|
|
||||||
if fd == 0 {
|
if fd == 0 {
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
runTime = &now
|
runTime = &now
|
||||||
SendMessage(Message{
|
} else {
|
||||||
Type: StartedMessage,
|
initSocketHook()
|
||||||
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initSocketHook()
|
|
||||||
go func() {
|
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
f := int(fd)
|
|
||||||
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
|
||||||
if tunListener != nil {
|
if tunListener != nil {
|
||||||
log.Infoln("TUN address: %v", tunListener.Address())
|
log.Infoln("TUN address: %v", tunListener.Address())
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
runTime = &now
|
runTime = &now
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getRunTime
|
|
||||||
func getRunTime() *C.char {
|
|
||||||
if runTime == nil {
|
|
||||||
return C.CString("")
|
|
||||||
}
|
}
|
||||||
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
|
return handleGetRunTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
//export stopTun
|
func handleStopTun() {
|
||||||
func stopTun() {
|
tunLock.Lock()
|
||||||
|
defer tunLock.Unlock()
|
||||||
removeSocketHook()
|
removeSocketHook()
|
||||||
go func() {
|
runTime = nil
|
||||||
tunLock.Lock()
|
if tunListener != nil {
|
||||||
defer tunLock.Unlock()
|
log.Infoln("TUN close")
|
||||||
|
_ = tunListener.Close()
|
||||||
runTime = nil
|
}
|
||||||
|
|
||||||
if tunListener != nil {
|
|
||||||
_ = tunListener.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//export setFdMap
|
func handleGetRunTime() string {
|
||||||
func setFdMap(fd C.long) {
|
if runTime == nil {
|
||||||
fdInt := int64(fd)
|
return ""
|
||||||
go func() {
|
}
|
||||||
fdMap.Store(fdInt)
|
return strconv.FormatInt(runTime.UnixMilli(), 10)
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func markSocket(fd Fd) {
|
func handleSetProcessMap(params string) {
|
||||||
SendMessage(Message{
|
var processMapItem = &ProcessMapItem{}
|
||||||
Type: ProtectMessage,
|
err := json.Unmarshal([]byte(params), processMapItem)
|
||||||
|
if err == nil {
|
||||||
|
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export attachInvokePort
|
||||||
|
func attachInvokePort(mPort C.longlong) {
|
||||||
|
invokePort = int64(mPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendInvokeMessage(message InvokeMessage) {
|
||||||
|
if invokePort == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bridge.SendToPort(invokePort, message.Json())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMarkSocket(fd Fd) {
|
||||||
|
sendInvokeMessage(InvokeMessage{
|
||||||
|
Type: ProtectInvoke,
|
||||||
Data: fd,
|
Data: fd,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleParseProcess(process Process) {
|
||||||
|
sendInvokeMessage(InvokeMessage{
|
||||||
|
Type: ProcessInvoke,
|
||||||
|
Data: process,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetFdMap(id string) {
|
||||||
|
go func() {
|
||||||
|
fdInvokeMap.completer(id, "")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func initSocketHook() {
|
func initSocketHook() {
|
||||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||||
if platform.ShouldBlockConnection() {
|
if platform.ShouldBlockConnection() {
|
||||||
@@ -143,26 +180,14 @@ func initSocketHook() {
|
|||||||
}
|
}
|
||||||
return conn.Control(func(fd uintptr) {
|
return conn.Control(func(fd uintptr) {
|
||||||
fdInt := int64(fd)
|
fdInt := int64(fd)
|
||||||
timeout := time.After(500 * time.Millisecond)
|
id := utils.NewUUIDV1().String()
|
||||||
id := atomic.AddInt64(&fdCounter, 1)
|
|
||||||
|
|
||||||
markSocket(Fd{
|
handleMarkSocket(Fd{
|
||||||
Id: id,
|
Id: id,
|
||||||
Value: fdInt,
|
Value: fdInt,
|
||||||
})
|
})
|
||||||
|
|
||||||
for {
|
fdInvokeMap.await(id)
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
exists := fdMap.Load(id)
|
|
||||||
if exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,95 +201,158 @@ func init() {
|
|||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
return "", process.ErrInvalidNetwork
|
return "", process.ErrInvalidNetwork
|
||||||
}
|
}
|
||||||
id := atomic.AddInt64(&counter, 1)
|
id := utils.NewUUIDV1().String()
|
||||||
|
handleParseProcess(Process{
|
||||||
timeout := time.After(200 * time.Millisecond)
|
Id: id,
|
||||||
|
Metadata: metadata,
|
||||||
SendMessage(Message{
|
|
||||||
Type: ProcessMessage,
|
|
||||||
Data: Process{
|
|
||||||
Id: id,
|
|
||||||
Metadata: metadata,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
return processInvokeMap.await(id), nil
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleGetAndroidVpnOptions() string {
|
||||||
|
tunLock.Lock()
|
||||||
|
defer tunLock.Unlock()
|
||||||
|
options := state.AndroidVpnOptions{
|
||||||
|
Enable: state.CurrentState.VpnProps.Enable,
|
||||||
|
Port: currentConfig.General.MixedPort,
|
||||||
|
Ipv4Address: state.DefaultIpv4Address,
|
||||||
|
Ipv6Address: state.GetIpv6Address(),
|
||||||
|
AccessControl: state.CurrentState.VpnProps.AccessControl,
|
||||||
|
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
|
||||||
|
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
|
||||||
|
RouteAddress: currentConfig.General.Tun.RouteAddress,
|
||||||
|
BypassDomain: state.CurrentState.BypassDomain,
|
||||||
|
DnsServerAddress: state.GetDnsServerAddress(),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(options)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, result func(data interface{})) bool {
|
||||||
|
switch action.Method {
|
||||||
|
case startTunMethod:
|
||||||
|
data := action.Data.(string)
|
||||||
|
var fd int
|
||||||
|
_ = json.Unmarshal([]byte(data), &fd)
|
||||||
|
result(handleStartTun(fd))
|
||||||
|
return true
|
||||||
|
case stopTunMethod:
|
||||||
|
handleStopTun()
|
||||||
|
result(true)
|
||||||
|
return true
|
||||||
|
case getAndroidVpnOptionsMethod:
|
||||||
|
result(handleGetAndroidVpnOptions())
|
||||||
|
return true
|
||||||
|
case updateDnsMethod:
|
||||||
|
data := action.Data.(string)
|
||||||
|
handleUpdateDns(data)
|
||||||
|
result(true)
|
||||||
|
return true
|
||||||
|
case setFdMapMethod:
|
||||||
|
fdId := action.Data.(string)
|
||||||
|
handleSetFdMap(fdId)
|
||||||
|
result(true)
|
||||||
|
return true
|
||||||
|
case setProcessMapMethod:
|
||||||
|
data := action.Data.(string)
|
||||||
|
handleSetProcessMap(data)
|
||||||
|
result(true)
|
||||||
|
return true
|
||||||
|
case getRunTimeMethod:
|
||||||
|
result(handleGetRunTime())
|
||||||
|
return true
|
||||||
|
case getCurrentProfileNameMethod:
|
||||||
|
result(handleGetCurrentProfileName())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//export quickStart
|
||||||
|
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
dir := C.GoString(dirChar)
|
||||||
|
bytes := []byte(C.GoString(paramsChar))
|
||||||
|
stateParams := C.GoString(stateParamsChar)
|
||||||
|
go func() {
|
||||||
|
res := handleInitClash(dir)
|
||||||
|
if res == false {
|
||||||
|
bridge.SendToPort(i, "init error")
|
||||||
|
}
|
||||||
|
handleSetState(stateParams)
|
||||||
|
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export startTUN
|
||||||
|
func startTUN(fd C.int) *C.char {
|
||||||
|
f := int(fd)
|
||||||
|
return C.CString(handleStartTun(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getRunTime
|
||||||
|
func getRunTime() *C.char {
|
||||||
|
return C.CString(handleGetRunTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
//export stopTun
|
||||||
|
func stopTun() {
|
||||||
|
handleStopTun()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export setFdMap
|
||||||
|
func setFdMap(fdIdChar *C.char) {
|
||||||
|
fdId := C.GoString(fdIdChar)
|
||||||
|
handleSetFdMap(fdId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getCurrentProfileName
|
||||||
|
func getCurrentProfileName() *C.char {
|
||||||
|
return C.CString(handleGetCurrentProfileName())
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getAndroidVpnOptions
|
||||||
|
func getAndroidVpnOptions() *C.char {
|
||||||
|
return C.CString(handleGetAndroidVpnOptions())
|
||||||
|
}
|
||||||
|
|
||||||
|
//export setState
|
||||||
|
func setState(s *C.char) {
|
||||||
|
paramsString := C.GoString(s)
|
||||||
|
handleSetState(paramsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export updateDns
|
||||||
|
func updateDns(s *C.char) {
|
||||||
|
dnsList := C.GoString(s)
|
||||||
|
handleUpdateDns(dnsList)
|
||||||
|
}
|
||||||
|
|
||||||
//export setProcessMap
|
//export setProcessMap
|
||||||
func setProcessMap(s *C.char) {
|
func setProcessMap(s *C.char) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
paramsString := C.GoString(s)
|
paramsString := C.GoString(s)
|
||||||
go func() {
|
handleSetProcessMap(paramsString)
|
||||||
var processMapItem = &ProcessMapItem{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), processMapItem)
|
|
||||||
if err == nil {
|
|
||||||
processMap.Store(processMapItem.Id, processMapItem.Value)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getCurrentProfileName
|
|
||||||
func getCurrentProfileName() *C.char {
|
|
||||||
if state.CurrentState == nil {
|
|
||||||
return C.CString("")
|
|
||||||
}
|
|
||||||
return C.CString(state.CurrentState.CurrentProfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getAndroidVpnOptions
|
|
||||||
func getAndroidVpnOptions() *C.char {
|
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
options := state.AndroidVpnOptions{
|
|
||||||
Enable: state.CurrentState.Enable,
|
|
||||||
Port: currentConfig.General.MixedPort,
|
|
||||||
Ipv4Address: state.DefaultIpv4Address,
|
|
||||||
Ipv6Address: state.GetIpv6Address(),
|
|
||||||
AccessControl: state.CurrentState.AccessControl,
|
|
||||||
SystemProxy: state.CurrentState.SystemProxy,
|
|
||||||
AllowBypass: state.CurrentState.AllowBypass,
|
|
||||||
RouteAddress: state.CurrentState.RouteAddress,
|
|
||||||
BypassDomain: state.CurrentState.BypassDomain,
|
|
||||||
DnsServerAddress: state.GetDnsServerAddress(),
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(options)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error:", err)
|
|
||||||
return C.CString("")
|
|
||||||
}
|
|
||||||
return C.CString(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export setState
|
|
||||||
func setState(s *C.char) {
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
core/lib_no_android.go
Normal file
7
core/lib_no_android.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !android && cgo
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
func SendMessage(message Message) {
|
|
||||||
s, err := message.Json()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Action{
|
|
||||||
Method: messageMethod,
|
|
||||||
}.callback(s)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
//go:build cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
bridge "core/dart-bridge"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Port int64 = -1
|
|
||||||
ServicePort int64 = -1
|
|
||||||
)
|
|
||||||
|
|
||||||
func SendMessage(message Message) {
|
|
||||||
s, err := message.Json()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if handler, ok := messageHandlers[message.Type]; ok {
|
|
||||||
handler(s)
|
|
||||||
} else {
|
|
||||||
sendToPort(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageHandlers = map[MessageType]func(string) bool{
|
|
||||||
ProtectMessage: sendToServicePort,
|
|
||||||
ProcessMessage: sendToServicePort,
|
|
||||||
StartedMessage: conditionalSend,
|
|
||||||
LoadedMessage: conditionalSend,
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToPort(s string) bool {
|
|
||||||
return bridge.SendToPort(Port, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToServicePort(s string) bool {
|
|
||||||
return bridge.SendToPort(ServicePort, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func conditionalSend(s string) bool {
|
|
||||||
isSuccess := sendToPort(s)
|
|
||||||
if !isSuccess {
|
|
||||||
return sendToServicePort(s)
|
|
||||||
}
|
|
||||||
return isSuccess
|
|
||||||
}
|
|
||||||
151
core/server.go
151
core/server.go
@@ -10,10 +10,29 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var conn net.Conn = nil
|
var conn net.Conn
|
||||||
|
|
||||||
|
func sendMessage(message Message) {
|
||||||
|
res, err := message.Json()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
send(Action{
|
||||||
|
Method: messageMethod,
|
||||||
|
}.getResult(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(data []byte) {
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = conn.Write(append(data, []byte("\n")...))
|
||||||
|
}
|
||||||
|
|
||||||
func startServer(arg string) {
|
func startServer(arg string) {
|
||||||
|
|
||||||
_, err := strconv.Atoi(arg)
|
_, err := strconv.Atoi(arg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn, err = net.Dial("unix", arg)
|
conn, err = net.Dial("unix", arg)
|
||||||
} else {
|
} else {
|
||||||
@@ -42,132 +61,12 @@ func startServer(arg string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go handleAction(action)
|
go handleAction(action, func(data interface{}) {
|
||||||
|
send(action.getResult(data))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAction(action *Action) {
|
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||||
switch action.Method {
|
return false
|
||||||
case initClashMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
action.callback(handleInitClash(data))
|
|
||||||
return
|
|
||||||
case getIsInitMethod:
|
|
||||||
action.callback(handleGetIsInit())
|
|
||||||
return
|
|
||||||
case forceGcMethod:
|
|
||||||
handleForceGc()
|
|
||||||
return
|
|
||||||
case shutdownMethod:
|
|
||||||
action.callback(handleShutdown())
|
|
||||||
return
|
|
||||||
case validateConfigMethod:
|
|
||||||
data := []byte(action.Data.(string))
|
|
||||||
action.callback(handleValidateConfig(data))
|
|
||||||
return
|
|
||||||
case updateConfigMethod:
|
|
||||||
data := []byte(action.Data.(string))
|
|
||||||
action.callback(handleUpdateConfig(data))
|
|
||||||
return
|
|
||||||
case getProxiesMethod:
|
|
||||||
action.callback(handleGetProxies())
|
|
||||||
return
|
|
||||||
case changeProxyMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
handleChangeProxy(data, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case getTrafficMethod:
|
|
||||||
data := action.Data.(bool)
|
|
||||||
action.callback(handleGetTraffic(data))
|
|
||||||
return
|
|
||||||
case getTotalTrafficMethod:
|
|
||||||
data := action.Data.(bool)
|
|
||||||
action.callback(handleGetTotalTraffic(data))
|
|
||||||
return
|
|
||||||
case resetTrafficMethod:
|
|
||||||
handleResetTraffic()
|
|
||||||
return
|
|
||||||
case asyncTestDelayMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
handleAsyncTestDelay(data, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case getConnectionsMethod:
|
|
||||||
action.callback(handleGetConnections())
|
|
||||||
return
|
|
||||||
case closeConnectionsMethod:
|
|
||||||
action.callback(handleCloseConnections())
|
|
||||||
return
|
|
||||||
case closeConnectionMethod:
|
|
||||||
id := action.Data.(string)
|
|
||||||
action.callback(handleCloseConnection(id))
|
|
||||||
return
|
|
||||||
case getExternalProvidersMethod:
|
|
||||||
action.callback(handleGetExternalProviders())
|
|
||||||
return
|
|
||||||
case getExternalProviderMethod:
|
|
||||||
externalProviderName := action.Data.(string)
|
|
||||||
action.callback(handleGetExternalProvider(externalProviderName))
|
|
||||||
case updateGeoDataMethod:
|
|
||||||
paramsString := action.Data.(string)
|
|
||||||
var params = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
action.callback(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
geoType := params["geoType"]
|
|
||||||
geoName := params["geoName"]
|
|
||||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case updateExternalProviderMethod:
|
|
||||||
providerName := action.Data.(string)
|
|
||||||
handleUpdateExternalProvider(providerName, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case sideLoadExternalProviderMethod:
|
|
||||||
paramsString := action.Data.(string)
|
|
||||||
var params = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
action.callback(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
providerName := params["providerName"]
|
|
||||||
data := params["data"]
|
|
||||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case startLogMethod:
|
|
||||||
handleStartLog()
|
|
||||||
return
|
|
||||||
case stopLogMethod:
|
|
||||||
handleStopLog()
|
|
||||||
return
|
|
||||||
case startListenerMethod:
|
|
||||||
action.callback(handleStartListener())
|
|
||||||
return
|
|
||||||
case stopListenerMethod:
|
|
||||||
action.callback(handleStopListener())
|
|
||||||
return
|
|
||||||
case getCountryCodeMethod:
|
|
||||||
ip := action.Data.(string)
|
|
||||||
handleGetCountryCode(ip, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case getMemoryMethod:
|
|
||||||
handleGetMemory(func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build android && cgo
|
|
||||||
|
|
||||||
package state
|
package state
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
var DefaultIpv4Address = "172.19.0.1/30"
|
var DefaultIpv4Address = "172.19.0.1/30"
|
||||||
var DefaultDnsAddress = "172.19.0.2"
|
var DefaultDnsAddress = "172.19.0.2"
|
||||||
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
||||||
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
|
|||||||
AllowBypass bool `json:"allowBypass"`
|
AllowBypass bool `json:"allowBypass"`
|
||||||
SystemProxy bool `json:"systemProxy"`
|
SystemProxy bool `json:"systemProxy"`
|
||||||
BypassDomain []string `json:"bypassDomain"`
|
BypassDomain []string `json:"bypassDomain"`
|
||||||
RouteAddress []string `json:"routeAddress"`
|
RouteAddress []netip.Prefix `json:"routeAddress"`
|
||||||
Ipv4Address string `json:"ipv4Address"`
|
Ipv4Address string `json:"ipv4Address"`
|
||||||
Ipv6Address string `json:"ipv6Address"`
|
Ipv6Address string `json:"ipv6Address"`
|
||||||
DnsServerAddress string `json:"dnsServerAddress"`
|
DnsServerAddress string `json:"dnsServerAddress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessControl struct {
|
type AccessControl struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
AcceptList []string `json:"acceptList"`
|
AcceptList []string `json:"acceptList"`
|
||||||
RejectList []string `json:"rejectList"`
|
RejectList []string `json:"rejectList"`
|
||||||
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
|
|||||||
AccessControl *AccessControl `json:"accessControl"`
|
AccessControl *AccessControl `json:"accessControl"`
|
||||||
AllowBypass bool `json:"allowBypass"`
|
AllowBypass bool `json:"allowBypass"`
|
||||||
SystemProxy bool `json:"systemProxy"`
|
SystemProxy bool `json:"systemProxy"`
|
||||||
RouteAddress []string `json:"routeAddress"`
|
|
||||||
Ipv6 bool `json:"ipv6"`
|
Ipv6 bool `json:"ipv6"`
|
||||||
BypassDomain []string `json:"bypassDomain"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
AndroidVpnRawOptions
|
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
|
||||||
CurrentProfileName string `json:"currentProfileName"`
|
CurrentProfileName string `json:"current-profile-name"`
|
||||||
|
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||||
|
BypassDomain []string `json:"bypass-domain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var CurrentState = &State{}
|
var CurrentState = &State{
|
||||||
|
OnlyStatisticsProxy: false,
|
||||||
|
CurrentProfileName: "",
|
||||||
|
}
|
||||||
|
|
||||||
func GetIpv6Address() string {
|
func GetIpv6Address() string {
|
||||||
if CurrentState.Ipv6 {
|
if CurrentState.VpnProps.Ipv6 {
|
||||||
return DefaultIpv6Address
|
return DefaultIpv6Address
|
||||||
} else {
|
} else {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
|
|||||||
}
|
}
|
||||||
prefix4 = append(prefix4, tempPrefix4)
|
prefix4 = append(prefix4, tempPrefix4)
|
||||||
var prefix6 []netip.Prefix
|
var prefix6 []netip.Prefix
|
||||||
if state.CurrentState.Ipv6 {
|
if state.CurrentState.VpnProps.Ipv6 {
|
||||||
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("startTUN error:", err)
|
log.Errorln("startTUN error:", err)
|
||||||
|
|||||||
@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
|
|||||||
import 'package:fl_clash/manager/hotkey_manager.dart';
|
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||||
import 'package:fl_clash/manager/manager.dart';
|
import 'package:fl_clash/manager/manager.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
import 'models/models.dart';
|
import 'models/models.dart';
|
||||||
import 'pages/pages.dart';
|
import 'pages/pages.dart';
|
||||||
|
|
||||||
runAppWithPreferences(
|
class Application extends ConsumerStatefulWidget {
|
||||||
Widget child, {
|
|
||||||
required AppState appState,
|
|
||||||
required Config config,
|
|
||||||
required AppFlowingState appFlowingState,
|
|
||||||
required ClashConfig clashConfig,
|
|
||||||
}) {
|
|
||||||
runApp(MultiProvider(
|
|
||||||
providers: [
|
|
||||||
ChangeNotifierProvider<ClashConfig>(
|
|
||||||
create: (_) => clashConfig,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<Config>(
|
|
||||||
create: (_) => config,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<AppFlowingState>(
|
|
||||||
create: (_) => appFlowingState,
|
|
||||||
),
|
|
||||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
|
||||||
create: (_) => appState,
|
|
||||||
update: (_, config, clashConfig, appState) {
|
|
||||||
appState?.mode = clashConfig.mode;
|
|
||||||
appState?.selectedMap = config.currentSelectedMap;
|
|
||||||
return appState!;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
child: child,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
class Application extends StatefulWidget {
|
|
||||||
const Application({
|
const Application({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Application> createState() => ApplicationState();
|
ConsumerState<Application> createState() => ApplicationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApplicationState extends State<Application> {
|
class ApplicationState extends ConsumerState<Application> {
|
||||||
late SystemColorSchemes systemColorSchemes;
|
late ColorSchemes systemColorSchemes;
|
||||||
Timer? _autoUpdateGroupTaskTimer;
|
Timer? _autoUpdateGroupTaskTimer;
|
||||||
Timer? _autoUpdateProfilesTaskTimer;
|
Timer? _autoUpdateProfilesTaskTimer;
|
||||||
|
|
||||||
@@ -73,7 +43,7 @@ class ApplicationState extends State<Application> {
|
|||||||
ColorScheme _getAppColorScheme({
|
ColorScheme _getAppColorScheme({
|
||||||
required Brightness brightness,
|
required Brightness brightness,
|
||||||
int? primaryColor,
|
int? primaryColor,
|
||||||
required SystemColorSchemes systemColorSchemes,
|
required ColorSchemes systemColorSchemes,
|
||||||
}) {
|
}) {
|
||||||
if (primaryColor != null) {
|
if (primaryColor != null) {
|
||||||
return ColorScheme.fromSeed(
|
return ColorScheme.fromSeed(
|
||||||
@@ -81,7 +51,7 @@ class ApplicationState extends State<Application> {
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
|
return systemColorSchemes.getColorSchemeForBrightness(brightness);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +60,21 @@ class ApplicationState extends State<Application> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_autoUpdateGroupTask();
|
_autoUpdateGroupTask();
|
||||||
_autoUpdateProfilesTask();
|
_autoUpdateProfilesTask();
|
||||||
globalState.appController = AppController(context);
|
globalState.appController = AppController(context, ref);
|
||||||
globalState.measure = Measure.of(context);
|
globalState.measure = Measure.of(context);
|
||||||
|
ref.listenManual(themeSettingProvider.select((state) => state.fontFamily),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next) {
|
||||||
|
globalState.measure = Measure.of(
|
||||||
|
context,
|
||||||
|
fontFamily: next.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, fireImmediately: true);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
final currentContext = globalState.navigatorKey.currentContext;
|
final currentContext = globalState.navigatorKey.currentContext;
|
||||||
if (currentContext != null) {
|
if (currentContext != null) {
|
||||||
globalState.appController = AppController(currentContext);
|
globalState.appController = AppController(currentContext, ref);
|
||||||
}
|
}
|
||||||
await globalState.appController.init();
|
await globalState.appController.init();
|
||||||
globalState.appController.initLink();
|
globalState.appController.initLink();
|
||||||
@@ -153,7 +132,10 @@ class ApplicationState extends State<Application> {
|
|||||||
return AppStateManager(
|
return AppStateManager(
|
||||||
child: ClashManager(
|
child: ClashManager(
|
||||||
child: ConnectivityManager(
|
child: ConnectivityManager(
|
||||||
onConnectivityChanged: globalState.appController.updateLocalIp,
|
onConnectivityChanged: () {
|
||||||
|
globalState.appController.updateLocalIp();
|
||||||
|
globalState.appController.addCheckIpNumDebounce();
|
||||||
|
},
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -164,7 +146,7 @@ class ApplicationState extends State<Application> {
|
|||||||
ColorScheme? lightDynamic,
|
ColorScheme? lightDynamic,
|
||||||
ColorScheme? darkDynamic,
|
ColorScheme? darkDynamic,
|
||||||
) {
|
) {
|
||||||
systemColorSchemes = SystemColorSchemes(
|
systemColorSchemes = ColorSchemes(
|
||||||
lightColorScheme: lightDynamic,
|
lightColorScheme: lightDynamic,
|
||||||
darkColorScheme: darkDynamic,
|
darkColorScheme: darkDynamic,
|
||||||
);
|
);
|
||||||
@@ -175,17 +157,13 @@ class ApplicationState extends State<Application> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(context) {
|
Widget build(context) {
|
||||||
return _buildWrap(
|
return _buildPlatformWrap(
|
||||||
_buildPlatformWrap(
|
_buildWrap(
|
||||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
Consumer(
|
||||||
selector: (_, appState, config) => ApplicationSelectorState(
|
builder: (_, ref, child) {
|
||||||
locale: config.appSetting.locale,
|
final locale =
|
||||||
themeMode: config.themeProps.themeMode,
|
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||||
primaryColor: config.themeProps.primaryColor,
|
final themeProps = ref.watch(themeSettingProvider);
|
||||||
prueBlack: config.themeProps.prueBlack,
|
|
||||||
fontFamily: config.themeProps.fontFamily,
|
|
||||||
),
|
|
||||||
builder: (_, state, child) {
|
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||||
@@ -201,11 +179,9 @@ class ApplicationState extends State<Application> {
|
|||||||
return MessageManager(
|
return MessageManager(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (_, container) {
|
builder: (_, container) {
|
||||||
final appController = globalState.appController;
|
globalState.appController.updateViewWidth(
|
||||||
final maxWidth = container.maxWidth;
|
container.maxWidth,
|
||||||
if (appController.appState.viewWidth != maxWidth) {
|
);
|
||||||
globalState.appController.updateViewWidth(maxWidth);
|
|
||||||
}
|
|
||||||
return _buildPage(child!);
|
return _buildPage(child!);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -213,28 +189,28 @@ class ApplicationState extends State<Application> {
|
|||||||
},
|
},
|
||||||
scrollBehavior: BaseScrollBehavior(),
|
scrollBehavior: BaseScrollBehavior(),
|
||||||
title: appName,
|
title: appName,
|
||||||
locale: other.getLocaleForString(state.locale),
|
locale: other.getLocaleForString(locale),
|
||||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||||
themeMode: state.themeMode,
|
themeMode: themeProps.themeMode,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
fontFamily: state.fontFamily.value,
|
fontFamily: themeProps.fontFamily.value,
|
||||||
pageTransitionsTheme: _pageTransitionsTheme,
|
pageTransitionsTheme: _pageTransitionsTheme,
|
||||||
colorScheme: _getAppColorScheme(
|
colorScheme: _getAppColorScheme(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
systemColorSchemes: systemColorSchemes,
|
systemColorSchemes: systemColorSchemes,
|
||||||
primaryColor: state.primaryColor,
|
primaryColor: themeProps.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
fontFamily: state.fontFamily.value,
|
fontFamily: themeProps.fontFamily.value,
|
||||||
pageTransitionsTheme: _pageTransitionsTheme,
|
pageTransitionsTheme: _pageTransitionsTheme,
|
||||||
colorScheme: _getAppColorScheme(
|
colorScheme: _getAppColorScheme(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
systemColorSchemes: systemColorSchemes,
|
systemColorSchemes: systemColorSchemes,
|
||||||
primaryColor: state.primaryColor,
|
primaryColor: themeProps.primaryColor,
|
||||||
).toPrueBlack(state.prueBlack),
|
).toPrueBlack(themeProps.prueBlack),
|
||||||
),
|
),
|
||||||
home: child,
|
home: child,
|
||||||
);
|
);
|
||||||
@@ -252,7 +228,7 @@ class ApplicationState extends State<Application> {
|
|||||||
linkManager.destroy();
|
linkManager.destroy();
|
||||||
_autoUpdateGroupTaskTimer?.cancel();
|
_autoUpdateGroupTaskTimer?.cancel();
|
||||||
_autoUpdateProfilesTaskTimer?.cancel();
|
_autoUpdateProfilesTaskTimer?.cancel();
|
||||||
await clashService?.destroy();
|
await clashCore.destroy();
|
||||||
await globalState.appController.savePreferences();
|
await globalState.appController.savePreferences();
|
||||||
await globalState.appController.handleExit();
|
await globalState.appController.handleExit();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import 'package:path/path.dart';
|
|||||||
|
|
||||||
class ClashCore {
|
class ClashCore {
|
||||||
static ClashCore? _instance;
|
static ClashCore? _instance;
|
||||||
late ClashInterface clashInterface;
|
late ClashHandlerInterface clashInterface;
|
||||||
|
|
||||||
ClashCore._internal() {
|
ClashCore._internal() {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -28,8 +28,12 @@ class ClashCore {
|
|||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initGeo() async {
|
Future<bool> preload() {
|
||||||
final homePath = await appPath.getHomeDirPath();
|
return clashInterface.preload();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> initGeo() async {
|
||||||
|
final homePath = await appPath.homeDirPath;
|
||||||
final homeDir = Directory(homePath);
|
final homeDir = Directory(homePath);
|
||||||
final isExists = await homeDir.exists();
|
final isExists = await homeDir.exists();
|
||||||
if (!isExists) {
|
if (!isExists) {
|
||||||
@@ -59,15 +63,16 @@ class ClashCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> init({
|
Future<bool> init() async {
|
||||||
required ClashConfig clashConfig,
|
await initGeo();
|
||||||
required Config config,
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
}) async {
|
|
||||||
await _initGeo();
|
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
|
||||||
return await clashInterface.init(homeDirPath);
|
return await clashInterface.init(homeDirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setState(CoreState state) async {
|
||||||
|
return await clashInterface.setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
shutdown() async {
|
shutdown() async {
|
||||||
await clashInterface.shutdown();
|
await clashInterface.shutdown();
|
||||||
}
|
}
|
||||||
@@ -135,6 +140,9 @@ class ClashCore {
|
|||||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
Future<List<ExternalProvider>> getExternalProviders() async {
|
||||||
final externalProvidersRawString =
|
final externalProvidersRawString =
|
||||||
await clashInterface.getExternalProviders();
|
await clashInterface.getExternalProviders();
|
||||||
|
if (externalProvidersRawString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return Isolate.run<List<ExternalProvider>>(
|
return Isolate.run<List<ExternalProvider>>(
|
||||||
() {
|
() {
|
||||||
final externalProviders =
|
final externalProviders =
|
||||||
@@ -152,7 +160,7 @@ class ClashCore {
|
|||||||
String externalProviderName) async {
|
String externalProviderName) async {
|
||||||
final externalProvidersRawString =
|
final externalProvidersRawString =
|
||||||
await clashInterface.getExternalProvider(externalProviderName);
|
await clashInterface.getExternalProvider(externalProviderName);
|
||||||
if (externalProvidersRawString == null) {
|
if (externalProvidersRawString.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (externalProvidersRawString.isEmpty) {
|
if (externalProvidersRawString.isEmpty) {
|
||||||
@@ -161,11 +169,8 @@ class ClashCore {
|
|||||||
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> updateGeoData({
|
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||||
required String geoType,
|
return clashInterface.updateGeoData(params);
|
||||||
required String geoName,
|
|
||||||
}) {
|
|
||||||
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> sideLoadExternalProvider({
|
Future<String> sideLoadExternalProvider({
|
||||||
@@ -190,13 +195,16 @@ class ClashCore {
|
|||||||
await clashInterface.stopListener();
|
await clashInterface.stopListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Delay> getDelay(String proxyName) async {
|
Future<Delay> getDelay(String url, String proxyName) async {
|
||||||
final data = await clashInterface.asyncTestDelay(proxyName);
|
final data = await clashInterface.asyncTestDelay(url, proxyName);
|
||||||
return Delay.fromJson(json.decode(data));
|
return Delay.fromJson(json.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Traffic> getTraffic(bool value) async {
|
Future<Traffic> getTraffic() async {
|
||||||
final trafficString = await clashInterface.getTraffic(value);
|
final trafficString = await clashInterface.getTraffic();
|
||||||
|
if (trafficString.isEmpty) {
|
||||||
|
return Traffic();
|
||||||
|
}
|
||||||
return Traffic.fromMap(json.decode(trafficString));
|
return Traffic.fromMap(json.decode(trafficString));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,16 +219,30 @@ class ClashCore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Traffic> getTotalTraffic(bool value) async {
|
Future<Traffic> getTotalTraffic() async {
|
||||||
final totalTrafficString = await clashInterface.getTotalTraffic(value);
|
final totalTrafficString = await clashInterface.getTotalTraffic();
|
||||||
|
if (totalTrafficString.isEmpty) {
|
||||||
|
return Traffic();
|
||||||
|
}
|
||||||
return Traffic.fromMap(json.decode(totalTrafficString));
|
return Traffic.fromMap(json.decode(totalTrafficString));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getMemory() async {
|
Future<int> getMemory() async {
|
||||||
final value = await clashInterface.getMemory();
|
final value = await clashInterface.getMemory();
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return int.parse(value);
|
return int.parse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ClashConfig?> getProfile(String id) async {
|
||||||
|
final res = await clashInterface.getProfile(id);
|
||||||
|
if (res.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ClashConfig.fromJson(json.decode(res));
|
||||||
|
}
|
||||||
|
|
||||||
resetTraffic() {
|
resetTraffic() {
|
||||||
clashInterface.resetTraffic();
|
clashInterface.resetTraffic();
|
||||||
}
|
}
|
||||||
@@ -236,6 +258,10 @@ class ClashCore {
|
|||||||
requestGc() {
|
requestGc() {
|
||||||
clashInterface.forceGc();
|
clashInterface.forceGc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() async {
|
||||||
|
await clashInterface.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final clashCore = ClashCore();
|
final clashCore = ClashCore();
|
||||||
|
|||||||
@@ -2362,18 +2362,39 @@ class ClashFFI {
|
|||||||
late final _initNativeApiBridge = _initNativeApiBridgePtr
|
late final _initNativeApiBridge = _initNativeApiBridgePtr
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||||
|
|
||||||
void initMessage(
|
void attachMessagePort(
|
||||||
int port,
|
int mPort,
|
||||||
) {
|
) {
|
||||||
return _initMessage(
|
return _attachMessagePort(
|
||||||
port,
|
mPort,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _initMessagePtr =
|
late final _attachMessagePortPtr =
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||||
'initMessage');
|
'attachMessagePort');
|
||||||
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
|
late final _attachMessagePort =
|
||||||
|
_attachMessagePortPtr.asFunction<void Function(int)>();
|
||||||
|
|
||||||
|
ffi.Pointer<ffi.Char> getTraffic() {
|
||||||
|
return _getTraffic();
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _getTrafficPtr =
|
||||||
|
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||||
|
'getTraffic');
|
||||||
|
late final _getTraffic =
|
||||||
|
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||||
|
|
||||||
|
ffi.Pointer<ffi.Char> getTotalTraffic() {
|
||||||
|
return _getTotalTraffic();
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _getTotalTrafficPtr =
|
||||||
|
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||||
|
'getTotalTraffic');
|
||||||
|
late final _getTotalTraffic =
|
||||||
|
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||||
|
|
||||||
void freeCString(
|
void freeCString(
|
||||||
ffi.Pointer<ffi.Char> s,
|
ffi.Pointer<ffi.Char> s,
|
||||||
@@ -2389,19 +2410,22 @@ class ClashFFI {
|
|||||||
late final _freeCString =
|
late final _freeCString =
|
||||||
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
int initClash(
|
void invokeAction(
|
||||||
ffi.Pointer<ffi.Char> homeDirStr,
|
ffi.Pointer<ffi.Char> paramsChar,
|
||||||
|
int port,
|
||||||
) {
|
) {
|
||||||
return _initClash(
|
return _invokeAction(
|
||||||
homeDirStr,
|
paramsChar,
|
||||||
|
port,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _initClashPtr =
|
late final _invokeActionPtr = _lookup<
|
||||||
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
|
ffi.NativeFunction<
|
||||||
'initClash');
|
ffi.Void Function(
|
||||||
late final _initClash =
|
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('invokeAction');
|
||||||
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
|
late final _invokeAction =
|
||||||
|
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||||
|
|
||||||
void startListener() {
|
void startListener() {
|
||||||
return _startListener();
|
return _startListener();
|
||||||
@@ -2419,317 +2443,55 @@ class ClashFFI {
|
|||||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
|
||||||
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
|
||||||
|
|
||||||
int getIsInit() {
|
void attachInvokePort(
|
||||||
return _getIsInit();
|
int mPort,
|
||||||
|
) {
|
||||||
|
return _attachInvokePort(
|
||||||
|
mPort,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _getIsInitPtr =
|
late final _attachInvokePortPtr =
|
||||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
|
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||||
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
|
'attachInvokePort');
|
||||||
|
late final _attachInvokePort =
|
||||||
|
_attachInvokePortPtr.asFunction<void Function(int)>();
|
||||||
|
|
||||||
int shutdownClash() {
|
void quickStart(
|
||||||
return _shutdownClash();
|
ffi.Pointer<ffi.Char> dirChar,
|
||||||
}
|
ffi.Pointer<ffi.Char> paramsChar,
|
||||||
|
ffi.Pointer<ffi.Char> stateParamsChar,
|
||||||
late final _shutdownClashPtr =
|
|
||||||
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
|
|
||||||
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
|
|
||||||
|
|
||||||
void forceGc() {
|
|
||||||
return _forceGc();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _forceGcPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
|
|
||||||
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
|
|
||||||
|
|
||||||
void validateConfig(
|
|
||||||
ffi.Pointer<ffi.Char> s,
|
|
||||||
int port,
|
int port,
|
||||||
) {
|
) {
|
||||||
return _validateConfig(
|
return _quickStart(
|
||||||
s,
|
dirChar,
|
||||||
|
paramsChar,
|
||||||
|
stateParamsChar,
|
||||||
port,
|
port,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _validateConfigPtr = _lookup<
|
late final _quickStartPtr = _lookup<
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(
|
|
||||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
|
|
||||||
late final _validateConfig = _validateConfigPtr
|
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
void updateConfig(
|
|
||||||
ffi.Pointer<ffi.Char> s,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _updateConfig(
|
|
||||||
s,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _updateConfigPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(
|
|
||||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateConfig');
|
|
||||||
late final _updateConfig =
|
|
||||||
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getProxies() {
|
|
||||||
return _getProxies();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getProxiesPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
|
||||||
'getProxies');
|
|
||||||
late final _getProxies =
|
|
||||||
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
|
||||||
|
|
||||||
void changeProxy(
|
|
||||||
ffi.Pointer<ffi.Char> s,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _changeProxy(
|
|
||||||
s,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _changeProxyPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(
|
|
||||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('changeProxy');
|
|
||||||
late final _changeProxy =
|
|
||||||
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getTraffic(
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _getTraffic(
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getTrafficPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
|
||||||
'getTraffic');
|
|
||||||
late final _getTraffic =
|
|
||||||
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getTotalTraffic(
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _getTotalTraffic(
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getTotalTrafficPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
|
||||||
'getTotalTraffic');
|
|
||||||
late final _getTotalTraffic =
|
|
||||||
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
|
||||||
|
|
||||||
void resetTraffic() {
|
|
||||||
return _resetTraffic();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _resetTrafficPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
|
|
||||||
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
|
|
||||||
|
|
||||||
void asyncTestDelay(
|
|
||||||
ffi.Pointer<ffi.Char> s,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _asyncTestDelay(
|
|
||||||
s,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _asyncTestDelayPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(
|
|
||||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
|
|
||||||
late final _asyncTestDelay = _asyncTestDelayPtr
|
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getConnections() {
|
|
||||||
return _getConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getConnectionsPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
|
||||||
'getConnections');
|
|
||||||
late final _getConnections =
|
|
||||||
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
|
||||||
|
|
||||||
void getMemory(
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _getMemory(
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getMemoryPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('getMemory');
|
|
||||||
late final _getMemory = _getMemoryPtr.asFunction<void Function(int)>();
|
|
||||||
|
|
||||||
void closeConnections() {
|
|
||||||
return _closeConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _closeConnectionsPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('closeConnections');
|
|
||||||
late final _closeConnections =
|
|
||||||
_closeConnectionsPtr.asFunction<void Function()>();
|
|
||||||
|
|
||||||
void closeConnection(
|
|
||||||
ffi.Pointer<ffi.Char> id,
|
|
||||||
) {
|
|
||||||
return _closeConnection(
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _closeConnectionPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
|
||||||
'closeConnection');
|
|
||||||
late final _closeConnection =
|
|
||||||
_closeConnectionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getExternalProviders() {
|
|
||||||
return _getExternalProviders();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getExternalProvidersPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
|
||||||
'getExternalProviders');
|
|
||||||
late final _getExternalProviders =
|
|
||||||
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getExternalProvider(
|
|
||||||
ffi.Pointer<ffi.Char> externalProviderNameChar,
|
|
||||||
) {
|
|
||||||
return _getExternalProvider(
|
|
||||||
externalProviderNameChar,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getExternalProviderPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Pointer<ffi.Char> Function(
|
|
||||||
ffi.Pointer<ffi.Char>)>>('getExternalProvider');
|
|
||||||
late final _getExternalProvider = _getExternalProviderPtr
|
|
||||||
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
|
||||||
|
|
||||||
void updateGeoData(
|
|
||||||
ffi.Pointer<ffi.Char> geoTypeChar,
|
|
||||||
ffi.Pointer<ffi.Char> geoNameChar,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _updateGeoData(
|
|
||||||
geoTypeChar,
|
|
||||||
geoNameChar,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _updateGeoDataPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
ffi.NativeFunction<
|
||||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||||
ffi.LongLong)>>('updateGeoData');
|
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('quickStart');
|
||||||
late final _updateGeoData = _updateGeoDataPtr.asFunction<
|
late final _quickStart = _quickStartPtr.asFunction<
|
||||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||||
|
ffi.Pointer<ffi.Char>, int)>();
|
||||||
|
|
||||||
void updateExternalProvider(
|
ffi.Pointer<ffi.Char> startTUN(
|
||||||
ffi.Pointer<ffi.Char> providerNameChar,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _updateExternalProvider(
|
|
||||||
providerNameChar,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _updateExternalProviderPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(
|
|
||||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
|
|
||||||
late final _updateExternalProvider = _updateExternalProviderPtr
|
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
void getCountryCode(
|
|
||||||
ffi.Pointer<ffi.Char> ipChar,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _getCountryCode(
|
|
||||||
ipChar,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _getCountryCodePtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(
|
|
||||||
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('getCountryCode');
|
|
||||||
late final _getCountryCode = _getCountryCodePtr
|
|
||||||
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
void sideLoadExternalProvider(
|
|
||||||
ffi.Pointer<ffi.Char> providerNameChar,
|
|
||||||
ffi.Pointer<ffi.Char> dataChar,
|
|
||||||
int port,
|
|
||||||
) {
|
|
||||||
return _sideLoadExternalProvider(
|
|
||||||
providerNameChar,
|
|
||||||
dataChar,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _sideLoadExternalProviderPtr = _lookup<
|
|
||||||
ffi.NativeFunction<
|
|
||||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
|
||||||
ffi.LongLong)>>('sideLoadExternalProvider');
|
|
||||||
late final _sideLoadExternalProvider =
|
|
||||||
_sideLoadExternalProviderPtr.asFunction<
|
|
||||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
|
||||||
|
|
||||||
void startLog() {
|
|
||||||
return _startLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _startLogPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('startLog');
|
|
||||||
late final _startLog = _startLogPtr.asFunction<void Function()>();
|
|
||||||
|
|
||||||
void stopLog() {
|
|
||||||
return _stopLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _stopLogPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
|
|
||||||
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
|
|
||||||
|
|
||||||
void startTUN(
|
|
||||||
int fd,
|
int fd,
|
||||||
int port,
|
|
||||||
) {
|
) {
|
||||||
return _startTUN(
|
return _startTUN(
|
||||||
fd,
|
fd,
|
||||||
port,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _startTUNPtr =
|
late final _startTUNPtr =
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
|
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
|
||||||
'startTUN');
|
'startTUN');
|
||||||
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
|
late final _startTUN =
|
||||||
|
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getRunTime() {
|
ffi.Pointer<ffi.Char> getRunTime() {
|
||||||
return _getRunTime();
|
return _getRunTime();
|
||||||
@@ -2750,30 +2512,18 @@ class ClashFFI {
|
|||||||
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
|
||||||
|
|
||||||
void setFdMap(
|
void setFdMap(
|
||||||
int fd,
|
ffi.Pointer<ffi.Char> fdIdChar,
|
||||||
) {
|
) {
|
||||||
return _setFdMap(
|
return _setFdMap(
|
||||||
fd,
|
fdIdChar,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _setFdMapPtr =
|
late final _setFdMapPtr =
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
|
|
||||||
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
|
|
||||||
|
|
||||||
void setProcessMap(
|
|
||||||
ffi.Pointer<ffi.Char> s,
|
|
||||||
) {
|
|
||||||
return _setProcessMap(
|
|
||||||
s,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final _setProcessMapPtr =
|
|
||||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||||
'setProcessMap');
|
'setFdMap');
|
||||||
late final _setProcessMap =
|
late final _setFdMap =
|
||||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||||
return _getCurrentProfileName();
|
return _getCurrentProfileName();
|
||||||
@@ -2822,6 +2572,20 @@ class ClashFFI {
|
|||||||
'updateDns');
|
'updateDns');
|
||||||
late final _updateDns =
|
late final _updateDns =
|
||||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
|
void setProcessMap(
|
||||||
|
ffi.Pointer<ffi.Char> s,
|
||||||
|
) {
|
||||||
|
return _setProcessMap(
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _setProcessMapPtr =
|
||||||
|
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||||
|
'setProcessMap');
|
||||||
|
late final _setProcessMap =
|
||||||
|
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final class __mbstate_t extends ffi.Union {
|
final class __mbstate_t extends ffi.Union {
|
||||||
@@ -3994,8 +3758,6 @@ final class GoSlice extends ffi.Struct {
|
|||||||
typedef GoInt = GoInt64;
|
typedef GoInt = GoInt64;
|
||||||
typedef GoInt64 = ffi.LongLong;
|
typedef GoInt64 = ffi.LongLong;
|
||||||
typedef DartGoInt64 = int;
|
typedef DartGoInt64 = int;
|
||||||
typedef GoUint8 = ffi.UnsignedChar;
|
|
||||||
typedef DartGoUint8 = int;
|
|
||||||
|
|
||||||
const int __has_safe_buffers = 1;
|
const int __has_safe_buffers = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:fl_clash/clash/message.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/models/models.dart';
|
||||||
|
|
||||||
mixin ClashInterface {
|
mixin ClashInterface {
|
||||||
FutureOr<bool> init(String homeDir);
|
Future<bool> init(String homeDir);
|
||||||
|
|
||||||
FutureOr<void> shutdown();
|
Future<bool> preload();
|
||||||
|
|
||||||
FutureOr<bool> get isInit;
|
Future<bool> shutdown();
|
||||||
|
|
||||||
forceGc();
|
Future<bool> get isInit;
|
||||||
|
|
||||||
|
Future<bool> forceGc();
|
||||||
|
|
||||||
FutureOr<String> validateConfig(String data);
|
FutureOr<String> validateConfig(String data);
|
||||||
|
|
||||||
Future<String> asyncTestDelay(String proxyName);
|
Future<String> asyncTestDelay(String url, String proxyName);
|
||||||
|
|
||||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
||||||
|
|
||||||
@@ -29,10 +35,7 @@ mixin ClashInterface {
|
|||||||
|
|
||||||
FutureOr<String>? getExternalProvider(String externalProviderName);
|
FutureOr<String>? getExternalProvider(String externalProviderName);
|
||||||
|
|
||||||
Future<String> updateGeoData({
|
Future<String> updateGeoData(UpdateGeoDataParams params);
|
||||||
required String geoType,
|
|
||||||
required String geoName,
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<String> sideLoadExternalProvider({
|
Future<String> sideLoadExternalProvider({
|
||||||
required String providerName,
|
required String providerName,
|
||||||
@@ -41,9 +44,9 @@ mixin ClashInterface {
|
|||||||
|
|
||||||
Future<String> updateExternalProvider(String providerName);
|
Future<String> updateExternalProvider(String providerName);
|
||||||
|
|
||||||
FutureOr<String> getTraffic(bool value);
|
FutureOr<String> getTraffic();
|
||||||
|
|
||||||
FutureOr<String> getTotalTraffic(bool value);
|
FutureOr<String> getTotalTraffic();
|
||||||
|
|
||||||
FutureOr<String> getCountryCode(String ip);
|
FutureOr<String> getCountryCode(String ip);
|
||||||
|
|
||||||
@@ -60,4 +63,361 @@ mixin ClashInterface {
|
|||||||
FutureOr<bool> closeConnection(String id);
|
FutureOr<bool> closeConnection(String id);
|
||||||
|
|
||||||
FutureOr<bool> closeConnections();
|
FutureOr<bool> closeConnections();
|
||||||
|
|
||||||
|
FutureOr<String> getProfile(String id);
|
||||||
|
|
||||||
|
Future<bool> setState(CoreState state);
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin AndroidClashInterface {
|
||||||
|
Future<bool> setFdMap(int fd);
|
||||||
|
|
||||||
|
Future<bool> setProcessMap(ProcessMapItem item);
|
||||||
|
|
||||||
|
Future<bool> stopTun();
|
||||||
|
|
||||||
|
Future<bool> updateDns(String value);
|
||||||
|
|
||||||
|
Future<DateTime?> startTun(int fd);
|
||||||
|
|
||||||
|
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||||
|
|
||||||
|
Future<String> getCurrentProfileName();
|
||||||
|
|
||||||
|
Future<DateTime?> getRunTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ClashHandlerInterface with ClashInterface {
|
||||||
|
Map<String, Completer> callbackCompleterMap = {};
|
||||||
|
|
||||||
|
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
|
||||||
|
Future.value(false);
|
||||||
|
|
||||||
|
handleResult(ActionResult result) async {
|
||||||
|
final completer = callbackCompleterMap[result.id];
|
||||||
|
try {
|
||||||
|
switch (result.method) {
|
||||||
|
case ActionMethod.initClash:
|
||||||
|
case ActionMethod.shutdown:
|
||||||
|
case ActionMethod.getIsInit:
|
||||||
|
case ActionMethod.startListener:
|
||||||
|
case ActionMethod.resetTraffic:
|
||||||
|
case ActionMethod.closeConnections:
|
||||||
|
case ActionMethod.closeConnection:
|
||||||
|
case ActionMethod.stopListener:
|
||||||
|
case ActionMethod.setState:
|
||||||
|
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 (_) {
|
||||||
|
commonPrint.log(result.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(String message);
|
||||||
|
|
||||||
|
reStart();
|
||||||
|
|
||||||
|
FutureOr<bool> destroy();
|
||||||
|
|
||||||
|
Future<T> invoke<T>({
|
||||||
|
required ActionMethod method,
|
||||||
|
dynamic data,
|
||||||
|
Duration? timeout,
|
||||||
|
FutureOr<T> Function()? onTimeout,
|
||||||
|
}) async {
|
||||||
|
final id = "${method.name}#${other.id}";
|
||||||
|
|
||||||
|
callbackCompleterMap[id] = Completer<T>();
|
||||||
|
|
||||||
|
dynamic defaultValue;
|
||||||
|
|
||||||
|
if (T == String) {
|
||||||
|
defaultValue = "";
|
||||||
|
}
|
||||||
|
if (T == bool) {
|
||||||
|
defaultValue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(
|
||||||
|
json.encode(
|
||||||
|
Action(
|
||||||
|
id: id,
|
||||||
|
method: method,
|
||||||
|
data: data,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
||||||
|
timeout: timeout,
|
||||||
|
onLast: () {
|
||||||
|
callbackCompleterMap.remove(id);
|
||||||
|
},
|
||||||
|
onTimeout: onTimeout ??
|
||||||
|
() {
|
||||||
|
return defaultValue;
|
||||||
|
},
|
||||||
|
functionName: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> init(String homeDir) {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.initClash,
|
||||||
|
data: homeDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> setState(CoreState state) {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.setState,
|
||||||
|
data: json.encode(state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
shutdown() async {
|
||||||
|
return await invoke<bool>(
|
||||||
|
method: ActionMethod.shutdown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> get isInit {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.getIsInit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> forceGc() {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.forceGc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> validateConfig(String data) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.validateConfig,
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||||
|
return await invoke<String>(
|
||||||
|
method: ActionMethod.updateConfig,
|
||||||
|
data: json.encode(updateConfigParams),
|
||||||
|
timeout: Duration(minutes: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getProxies() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getProxies,
|
||||||
|
timeout: Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.changeProxy,
|
||||||
|
data: json.encode(changeProxyParams),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getExternalProviders() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getExternalProviders,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getExternalProvider(String externalProviderName) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getExternalProvider,
|
||||||
|
data: externalProviderName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.updateGeoData,
|
||||||
|
data: json.encode(params),
|
||||||
|
timeout: Duration(minutes: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> sideLoadExternalProvider({
|
||||||
|
required String providerName,
|
||||||
|
required String data,
|
||||||
|
}) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.sideLoadExternalProvider,
|
||||||
|
data: json.encode({
|
||||||
|
"providerName": providerName,
|
||||||
|
"data": data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> updateExternalProvider(String providerName) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.updateExternalProvider,
|
||||||
|
data: providerName,
|
||||||
|
timeout: Duration(minutes: 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getConnections() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getConnections,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> closeConnections() {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.closeConnections,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> closeConnection(String id) {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.closeConnection,
|
||||||
|
data: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getProfile(String id) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getProfile,
|
||||||
|
data: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getTotalTraffic() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getTotalTraffic,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getTraffic() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getTraffic,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
resetTraffic() {
|
||||||
|
invoke(method: ActionMethod.resetTraffic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
startLog() {
|
||||||
|
invoke(method: ActionMethod.startLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
stopLog() {
|
||||||
|
invoke<bool>(
|
||||||
|
method: ActionMethod.stopLog,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> startListener() {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.startListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
stopListener() {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.stopListener,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> asyncTestDelay(String url, String proxyName) {
|
||||||
|
final delayParams = {
|
||||||
|
"proxy-name": proxyName,
|
||||||
|
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||||
|
"test-url": url,
|
||||||
|
};
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.asyncTestDelay,
|
||||||
|
data: json.encode(delayParams),
|
||||||
|
timeout: Duration(
|
||||||
|
milliseconds: 6000,
|
||||||
|
),
|
||||||
|
onTimeout: () {
|
||||||
|
return json.encode(
|
||||||
|
Delay(
|
||||||
|
name: proxyName,
|
||||||
|
value: -1,
|
||||||
|
url: url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getCountryCode(String ip) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getCountryCode,
|
||||||
|
data: ip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String> getMemory() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getMemory,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,28 +3,58 @@ import 'dart:convert';
|
|||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:fl_clash/common/constant.dart';
|
import 'package:fl_clash/common/constant.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/plugins/service.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
|
||||||
import 'generated/clash_ffi.dart';
|
import 'generated/clash_ffi.dart';
|
||||||
import 'interface.dart';
|
import 'interface.dart';
|
||||||
|
|
||||||
class ClashLib with ClashInterface {
|
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||||
static ClashLib? _instance;
|
static ClashLib? _instance;
|
||||||
final receiver = ReceivePort();
|
Completer<bool> _canSendCompleter = Completer();
|
||||||
|
SendPort? sendPort;
|
||||||
late final ClashFFI clashFFI;
|
final receiverPort = ReceivePort();
|
||||||
|
|
||||||
late final DynamicLibrary lib;
|
|
||||||
|
|
||||||
ClashLib._internal() {
|
ClashLib._internal() {
|
||||||
lib = DynamicLibrary.open("libclash.so");
|
_initService();
|
||||||
clashFFI = ClashFFI(lib);
|
}
|
||||||
clashFFI.initNativeApiBridge(
|
|
||||||
NativeApi.initializeApiDLData,
|
@override
|
||||||
);
|
preload() {
|
||||||
|
return _canSendCompleter.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
_initService() async {
|
||||||
|
await service?.destroy();
|
||||||
|
_registerMainPort(receiverPort.sendPort);
|
||||||
|
receiverPort.listen((message) {
|
||||||
|
if (message is SendPort) {
|
||||||
|
if (_canSendCompleter.isCompleted) {
|
||||||
|
sendPort = null;
|
||||||
|
_canSendCompleter = Completer();
|
||||||
|
}
|
||||||
|
sendPort = message;
|
||||||
|
_canSendCompleter.complete(true);
|
||||||
|
} else {
|
||||||
|
handleResult(
|
||||||
|
ActionResult.fromJson(json.decode(
|
||||||
|
message,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await service?.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
_registerMainPort(SendPort sendPort) {
|
||||||
|
IsolateNameServer.removePortNameMapping(mainIsolate);
|
||||||
|
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ClashLib() {
|
factory ClashLib() {
|
||||||
@@ -32,227 +62,145 @@ class ClashLib with ClashInterface {
|
|||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
initMessage() {
|
@override
|
||||||
clashFFI.initMessage(
|
Future<bool> nextHandleResult(result, completer) async {
|
||||||
receiver.sendPort.nativePort,
|
switch (result.method) {
|
||||||
);
|
case ActionMethod.setFdMap:
|
||||||
|
case ActionMethod.setProcessMap:
|
||||||
|
case ActionMethod.stopTun:
|
||||||
|
case ActionMethod.updateDns:
|
||||||
|
completer?.complete(result.data as bool);
|
||||||
|
return true;
|
||||||
|
case ActionMethod.getRunTime:
|
||||||
|
case ActionMethod.startTun:
|
||||||
|
case ActionMethod.getAndroidVpnOptions:
|
||||||
|
case ActionMethod.getCurrentProfileName:
|
||||||
|
completer?.complete(result.data as String);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool init(String homeDir) {
|
destroy() async {
|
||||||
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
|
await service?.destroy();
|
||||||
final isInit = clashFFI.initClash(homeDirChar) == 1;
|
|
||||||
malloc.free(homeDirChar);
|
|
||||||
return isInit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
shutdown() async {
|
|
||||||
clashFFI.shutdownClash();
|
|
||||||
lib.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isInit => clashFFI.getIsInit() == 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validateConfig(String data) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.validateConfig(
|
|
||||||
dataChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(dataChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final params = json.encode(updateConfigParams);
|
|
||||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateConfig(
|
|
||||||
paramsChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(paramsChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getProxies() {
|
|
||||||
final proxiesRaw = clashFFI.getProxies();
|
|
||||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(proxiesRaw);
|
|
||||||
return proxiesRawString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getExternalProviders() {
|
|
||||||
final externalProvidersRaw = clashFFI.getExternalProviders();
|
|
||||||
final externalProvidersRawString =
|
|
||||||
externalProvidersRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(externalProvidersRaw);
|
|
||||||
return externalProvidersRawString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getExternalProvider(String externalProviderName) {
|
|
||||||
final externalProviderNameChar =
|
|
||||||
externalProviderName.toNativeUtf8().cast<Char>();
|
|
||||||
final externalProviderRaw =
|
|
||||||
clashFFI.getExternalProvider(externalProviderNameChar);
|
|
||||||
malloc.free(externalProviderNameChar);
|
|
||||||
final externalProviderRawString =
|
|
||||||
externalProviderRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(externalProviderRaw);
|
|
||||||
return externalProviderRawString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateGeoData({
|
|
||||||
required String geoType,
|
|
||||||
required String geoName,
|
|
||||||
}) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
|
|
||||||
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateGeoData(
|
|
||||||
geoTypeChar,
|
|
||||||
geoNameChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(geoTypeChar);
|
|
||||||
malloc.free(geoNameChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> sideLoadExternalProvider({
|
|
||||||
required String providerName,
|
|
||||||
required String data,
|
|
||||||
}) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
|
||||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.sideLoadExternalProvider(
|
|
||||||
providerNameChar,
|
|
||||||
dataChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(providerNameChar);
|
|
||||||
malloc.free(dataChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateExternalProvider(String providerName) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateExternalProvider(
|
|
||||||
providerNameChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(providerNameChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final params = json.encode(changeProxyParams);
|
|
||||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.changeProxy(
|
|
||||||
paramsChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(paramsChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getConnections() {
|
|
||||||
final connectionsDataRaw = clashFFI.getConnections();
|
|
||||||
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(connectionsDataRaw);
|
|
||||||
return connectionsString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
closeConnection(String id) {
|
|
||||||
final idChar = id.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.closeConnection(idChar);
|
|
||||||
malloc.free(idChar);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
closeConnections() {
|
reStart() {
|
||||||
clashFFI.closeConnections();
|
_initService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> shutdown() async {
|
||||||
|
await super.shutdown();
|
||||||
|
destroy();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
startListener() async {
|
sendMessage(String message) async {
|
||||||
clashFFI.startListener();
|
await _canSendCompleter.future;
|
||||||
return true;
|
sendPort?.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
stopListener() async {
|
Future<bool> setFdMap(int fd) {
|
||||||
clashFFI.stopListener();
|
return invoke<bool>(
|
||||||
return true;
|
method: ActionMethod.setFdMap,
|
||||||
|
data: json.encode(fd),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> asyncTestDelay(String proxyName) {
|
Future<bool> setProcessMap(item) {
|
||||||
final delayParams = {
|
return invoke<bool>(
|
||||||
"proxy-name": proxyName,
|
method: ActionMethod.setProcessMap,
|
||||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
data: item,
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DateTime?> startTun(int fd) async {
|
||||||
|
final res = await invoke<String>(
|
||||||
|
method: ActionMethod.startTun,
|
||||||
|
data: json.encode(fd),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> stopTun() {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.stopTun,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||||
|
final res = await invoke<String>(
|
||||||
|
method: ActionMethod.getAndroidVpnOptions,
|
||||||
|
);
|
||||||
|
if (res.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return AndroidVpnOptions.fromJson(json.decode(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> updateDns(String value) {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.updateDns,
|
||||||
|
data: value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DateTime?> getRunTime() async {
|
||||||
|
final runTimeString = await invoke<String>(
|
||||||
|
method: ActionMethod.getRunTime,
|
||||||
|
);
|
||||||
|
if (runTimeString.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getCurrentProfileName() {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getCurrentProfileName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClashLibHandler {
|
||||||
|
static ClashLibHandler? _instance;
|
||||||
|
|
||||||
|
late final ClashFFI clashFFI;
|
||||||
|
|
||||||
|
late final DynamicLibrary lib;
|
||||||
|
|
||||||
|
ClashLibHandler._internal() {
|
||||||
|
lib = DynamicLibrary.open("libclash.so");
|
||||||
|
clashFFI = ClashFFI(lib);
|
||||||
|
clashFFI.initNativeApiBridge(
|
||||||
|
NativeApi.initializeApiDLData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ClashLibHandler() {
|
||||||
|
_instance ??= ClashLibHandler._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> invokeAction(String actionParams) {
|
||||||
final completer = Completer<String>();
|
final completer = Completer<String>();
|
||||||
final receiver = ReceivePort();
|
final receiver = ReceivePort();
|
||||||
receiver.listen((message) {
|
receiver.listen((message) {
|
||||||
@@ -261,89 +209,33 @@ class ClashLib with ClashInterface {
|
|||||||
receiver.close();
|
receiver.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final delayParamsChar =
|
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
|
||||||
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
clashFFI.invokeAction(
|
||||||
clashFFI.asyncTestDelay(
|
actionParamsChar,
|
||||||
delayParamsChar,
|
|
||||||
receiver.sendPort.nativePort,
|
receiver.sendPort.nativePort,
|
||||||
);
|
);
|
||||||
malloc.free(delayParamsChar);
|
malloc.free(actionParamsChar);
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
attachMessagePort(int messagePort) {
|
||||||
String getTraffic(bool value) {
|
clashFFI.attachMessagePort(
|
||||||
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0);
|
messagePort,
|
||||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(trafficRaw);
|
|
||||||
return trafficString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getTotalTraffic(bool value) {
|
|
||||||
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
|
|
||||||
clashFFI.freeCString(trafficRaw);
|
|
||||||
return trafficRaw.cast<Utf8>().toDartString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void resetTraffic() {
|
|
||||||
clashFFI.resetTraffic();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void startLog() {
|
|
||||||
clashFFI.startLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopLog() {
|
|
||||||
clashFFI.stopLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
forceGc() {
|
|
||||||
clashFFI.forceGc();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getCountryCode(String ip) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final ipChar = ip.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.getCountryCode(
|
|
||||||
ipChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
);
|
||||||
malloc.free(ipChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
attachInvokePort(int invokePort) {
|
||||||
FutureOr<String> getMemory() {
|
clashFFI.attachInvokePort(
|
||||||
final completer = Completer<String>();
|
invokePort,
|
||||||
final receiver = ReceivePort();
|
);
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
clashFFI.getMemory(receiver.sendPort.nativePort);
|
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Android
|
DateTime? startTun(int fd) {
|
||||||
|
final runTimeRaw = clashFFI.startTUN(fd);
|
||||||
startTun(int fd, int port) {
|
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||||
if (!Platform.isAndroid) return;
|
clashFFI.freeCString(runTimeRaw);
|
||||||
clashFFI.startTUN(fd, port);
|
if (runTimeString.isEmpty) return null;
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTun() {
|
stopTun() {
|
||||||
@@ -351,7 +243,6 @@ class ClashLib with ClashInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDns(String dns) {
|
updateDns(String dns) {
|
||||||
if (!Platform.isAndroid) return;
|
|
||||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||||
clashFFI.updateDns(dnsChar);
|
clashFFI.updateDns(dnsChar);
|
||||||
malloc.free(dnsChar);
|
malloc.free(dnsChar);
|
||||||
@@ -359,7 +250,7 @@ class ClashLib with ClashInterface {
|
|||||||
|
|
||||||
setProcessMap(ProcessMapItem processMapItem) {
|
setProcessMap(ProcessMapItem processMapItem) {
|
||||||
final processMapItemChar =
|
final processMapItemChar =
|
||||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||||
clashFFI.setProcessMap(processMapItemChar);
|
clashFFI.setProcessMap(processMapItemChar);
|
||||||
malloc.free(processMapItemChar);
|
malloc.free(processMapItemChar);
|
||||||
}
|
}
|
||||||
@@ -384,8 +275,70 @@ class ClashLib with ClashInterface {
|
|||||||
return AndroidVpnOptions.fromJson(vpnOptions);
|
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFdMap(int fd) {
|
Traffic getTraffic() {
|
||||||
clashFFI.setFdMap(fd);
|
final trafficRaw = clashFFI.getTraffic();
|
||||||
|
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||||
|
clashFFI.freeCString(trafficRaw);
|
||||||
|
if (trafficString.isEmpty) {
|
||||||
|
return Traffic();
|
||||||
|
}
|
||||||
|
return Traffic.fromMap(json.decode(trafficString));
|
||||||
|
}
|
||||||
|
|
||||||
|
Traffic getTotalTraffic(bool value) {
|
||||||
|
final trafficRaw = clashFFI.getTotalTraffic();
|
||||||
|
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||||
|
clashFFI.freeCString(trafficRaw);
|
||||||
|
if (trafficString.isEmpty) {
|
||||||
|
return Traffic();
|
||||||
|
}
|
||||||
|
return Traffic.fromMap(json.decode(trafficString));
|
||||||
|
}
|
||||||
|
|
||||||
|
startListener() async {
|
||||||
|
clashFFI.startListener();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopListener() async {
|
||||||
|
clashFFI.stopListener();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFdMap(String id) {
|
||||||
|
final idChar = id.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.setFdMap(idChar);
|
||||||
|
malloc.free(idChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> quickStart(
|
||||||
|
String homeDir,
|
||||||
|
UpdateConfigParams updateConfigParams,
|
||||||
|
CoreState state,
|
||||||
|
) {
|
||||||
|
final completer = Completer<String>();
|
||||||
|
final receiver = ReceivePort();
|
||||||
|
receiver.listen((message) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(message);
|
||||||
|
receiver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final params = json.encode(updateConfigParams);
|
||||||
|
final stateParams = json.encode(state);
|
||||||
|
final homeChar = homeDir.toNativeUtf8().cast<Char>();
|
||||||
|
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||||
|
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.quickStart(
|
||||||
|
homeChar,
|
||||||
|
paramsChar,
|
||||||
|
stateParamsChar,
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
|
malloc.free(homeChar);
|
||||||
|
malloc.free(paramsChar);
|
||||||
|
malloc.free(stateParamsChar);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime? getRunTime() {
|
DateTime? getRunTime() {
|
||||||
@@ -397,4 +350,5 @@ class ClashLib with ClashInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final clashLib = Platform.isAndroid ? ClashLib() : null;
|
ClashLib? get clashLib =>
|
||||||
|
Platform.isAndroid && !globalState.isService ? ClashLib() : null;
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class ClashMessage {
|
class ClashMessage {
|
||||||
final controller = StreamController();
|
final controller = StreamController<String>();
|
||||||
|
|
||||||
ClashMessage._() {
|
ClashMessage._() {
|
||||||
clashLib?.receiver.listen(controller.add);
|
|
||||||
controller.stream.listen(
|
controller.stream.listen(
|
||||||
(message) {
|
(message) {
|
||||||
|
if(message.isEmpty){
|
||||||
|
return;
|
||||||
|
}
|
||||||
final m = AppMessage.fromJson(json.decode(message));
|
final m = AppMessage.fromJson(json.decode(message));
|
||||||
for (final AppMessageListener listener in _listeners) {
|
for (final AppMessageListener listener in _listeners) {
|
||||||
switch (m.type) {
|
switch (m.type) {
|
||||||
@@ -25,9 +26,6 @@ class ClashMessage {
|
|||||||
case AppMessageType.request:
|
case AppMessageType.request:
|
||||||
listener.onRequest(Connection.fromJson(m.data));
|
listener.onRequest(Connection.fromJson(m.data));
|
||||||
break;
|
break;
|
||||||
case AppMessageType.started:
|
|
||||||
listener.onStarted(m.data);
|
|
||||||
break;
|
|
||||||
case AppMessageType.loaded:
|
case AppMessageType.loaded:
|
||||||
listener.onLoaded(m.data);
|
listener.onLoaded(m.data);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/clash/interface.dart';
|
import 'package:fl_clash/clash/interface.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/core.dart';
|
import 'package:fl_clash/models/core.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
|
||||||
class ClashService with ClashInterface {
|
class ClashService extends ClashHandlerInterface {
|
||||||
static ClashService? _instance;
|
static ClashService? _instance;
|
||||||
|
|
||||||
Completer<ServerSocket> serverCompleter = Completer();
|
Completer<ServerSocket> serverCompleter = Completer();
|
||||||
|
|
||||||
Completer<Socket> socketCompleter = Completer();
|
Completer<Socket> socketCompleter = Completer();
|
||||||
|
|
||||||
Map<String, Completer> callbackCompleterMap = {};
|
bool isStarting = false;
|
||||||
|
|
||||||
Process? process;
|
Process? process;
|
||||||
|
|
||||||
@@ -26,52 +25,66 @@ class ClashService with ClashInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ClashService._internal() {
|
ClashService._internal() {
|
||||||
_createServer();
|
_initServer();
|
||||||
startCore();
|
reStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
_createServer() async {
|
_initServer() async {
|
||||||
final address = !Platform.isWindows
|
runZonedGuarded(() async {
|
||||||
? InternetAddress(
|
final address = !Platform.isWindows
|
||||||
unixSocketPath,
|
? InternetAddress(
|
||||||
type: InternetAddressType.unix,
|
unixSocketPath,
|
||||||
)
|
type: InternetAddressType.unix,
|
||||||
: InternetAddress(
|
)
|
||||||
localhost,
|
: InternetAddress(
|
||||||
type: InternetAddressType.IPv4,
|
localhost,
|
||||||
);
|
type: InternetAddressType.IPv4,
|
||||||
await _deleteSocketFile();
|
);
|
||||||
final server = await ServerSocket.bind(
|
await _deleteSocketFile();
|
||||||
address,
|
final server = await ServerSocket.bind(
|
||||||
0,
|
address,
|
||||||
shared: true,
|
0,
|
||||||
);
|
shared: true,
|
||||||
serverCompleter.complete(server);
|
);
|
||||||
await for (final socket in server) {
|
serverCompleter.complete(server);
|
||||||
await _destroySocket();
|
await for (final socket in server) {
|
||||||
socketCompleter.complete(socket);
|
await _destroySocket();
|
||||||
socket
|
socketCompleter.complete(socket);
|
||||||
.transform(
|
socket
|
||||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
.transform(
|
||||||
handleData: (Uint8List data, EventSink<String> sink) {
|
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||||
sink.add(utf8.decode(data, allowMalformed: true));
|
handleData: (Uint8List data, EventSink<String> sink) {
|
||||||
|
sink.add(utf8.decode(data, allowMalformed: true));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.transform(LineSplitter())
|
||||||
|
.listen(
|
||||||
|
(data) {
|
||||||
|
handleResult(
|
||||||
|
ActionResult.fromJson(
|
||||||
|
json.decode(data.trim()),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
)
|
}
|
||||||
.transform(LineSplitter())
|
}, (error, stack) {
|
||||||
.listen(
|
commonPrint.log(error.toString());
|
||||||
(data) {
|
if(error is SocketException){
|
||||||
_handleAction(
|
globalState.showNotifier(error.toString());
|
||||||
Action.fromJson(
|
globalState.appController.restartCore();
|
||||||
json.decode(data.trim()),
|
}
|
||||||
),
|
});
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startCore() async {
|
@override
|
||||||
|
reStart() async {
|
||||||
|
if (isStarting == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isStarting = true;
|
||||||
|
socketCompleter = Completer();
|
||||||
if (process != null) {
|
if (process != null) {
|
||||||
await shutdown();
|
await shutdown();
|
||||||
}
|
}
|
||||||
@@ -93,6 +106,21 @@ class ClashService with ClashInterface {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
process!.stdout.listen((_) {});
|
process!.stdout.listen((_) {});
|
||||||
|
isStarting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
destroy() async {
|
||||||
|
final server = await serverCompleter.future;
|
||||||
|
await server.close();
|
||||||
|
await _deleteSocketFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
sendMessage(String message) async {
|
||||||
|
final socket = await socketCompleter.future;
|
||||||
|
socket.writeln(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_deleteSocketFile() async {
|
_deleteSocketFile() async {
|
||||||
@@ -112,327 +140,21 @@ class ClashService with ClashInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleAction(Action action) {
|
|
||||||
final completer = callbackCompleterMap[action.id];
|
|
||||||
switch (action.method) {
|
|
||||||
case ActionMethod.initClash:
|
|
||||||
case ActionMethod.shutdown:
|
|
||||||
case ActionMethod.getIsInit:
|
|
||||||
case ActionMethod.startListener:
|
|
||||||
case ActionMethod.resetTraffic:
|
|
||||||
case ActionMethod.closeConnections:
|
|
||||||
case ActionMethod.closeConnection:
|
|
||||||
case ActionMethod.stopListener:
|
|
||||||
completer?.complete(action.data as bool);
|
|
||||||
return;
|
|
||||||
case ActionMethod.changeProxy:
|
|
||||||
case ActionMethod.getProxies:
|
|
||||||
case ActionMethod.getTraffic:
|
|
||||||
case ActionMethod.getTotalTraffic:
|
|
||||||
case ActionMethod.asyncTestDelay:
|
|
||||||
case ActionMethod.getConnections:
|
|
||||||
case ActionMethod.getExternalProviders:
|
|
||||||
case ActionMethod.getExternalProvider:
|
|
||||||
case ActionMethod.validateConfig:
|
|
||||||
case ActionMethod.updateConfig:
|
|
||||||
case ActionMethod.updateGeoData:
|
|
||||||
case ActionMethod.updateExternalProvider:
|
|
||||||
case ActionMethod.sideLoadExternalProvider:
|
|
||||||
case ActionMethod.getCountryCode:
|
|
||||||
case ActionMethod.getMemory:
|
|
||||||
completer?.complete(action.data as String);
|
|
||||||
return;
|
|
||||||
case ActionMethod.message:
|
|
||||||
clashMessage.controller.add(action.data as String);
|
|
||||||
return;
|
|
||||||
case ActionMethod.forceGc:
|
|
||||||
case ActionMethod.startLog:
|
|
||||||
case ActionMethod.stopLog:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T> _invoke<T>({
|
|
||||||
required ActionMethod method,
|
|
||||||
dynamic data,
|
|
||||||
Duration? timeout,
|
|
||||||
FutureOr<T> Function()? onTimeout,
|
|
||||||
}) async {
|
|
||||||
final id = "${method.name}#${other.id}";
|
|
||||||
final socket = await socketCompleter.future;
|
|
||||||
callbackCompleterMap[id] = Completer<T>();
|
|
||||||
socket.writeln(
|
|
||||||
json.encode(
|
|
||||||
Action(
|
|
||||||
id: id,
|
|
||||||
method: method,
|
|
||||||
data: data,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
|
||||||
timeout: timeout,
|
|
||||||
onLast: () {
|
|
||||||
callbackCompleterMap.remove(id);
|
|
||||||
},
|
|
||||||
onTimeout: onTimeout ??
|
|
||||||
() {
|
|
||||||
if (T is String) {
|
|
||||||
return "" as T;
|
|
||||||
}
|
|
||||||
if (T is bool) {
|
|
||||||
return false as T;
|
|
||||||
}
|
|
||||||
return null as T;
|
|
||||||
},
|
|
||||||
functionName: id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_prueInvoke({
|
|
||||||
required ActionMethod method,
|
|
||||||
dynamic data,
|
|
||||||
}) async {
|
|
||||||
final id = "${method.name}#${other.id}";
|
|
||||||
final socket = await socketCompleter.future;
|
|
||||||
socket.writeln(
|
|
||||||
json.encode(
|
|
||||||
Action(
|
|
||||||
id: id,
|
|
||||||
method: method,
|
|
||||||
data: data,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> init(String homeDir) {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.initClash,
|
|
||||||
data: homeDir,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
shutdown() async {
|
shutdown() async {
|
||||||
await _invoke<bool>(
|
|
||||||
method: ActionMethod.shutdown,
|
|
||||||
);
|
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
await request.stopCoreByHelper();
|
await request.stopCoreByHelper();
|
||||||
}
|
}
|
||||||
await _destroySocket();
|
await _destroySocket();
|
||||||
process?.kill();
|
process?.kill();
|
||||||
process = null;
|
process = null;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> get isInit {
|
Future<bool> preload() async {
|
||||||
return _invoke<bool>(
|
await serverCompleter.future;
|
||||||
method: ActionMethod.getIsInit,
|
return true;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
forceGc() {
|
|
||||||
_prueInvoke(method: ActionMethod.forceGc);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> validateConfig(String data) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.validateConfig,
|
|
||||||
data: data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
|
||||||
return await _invoke<String>(
|
|
||||||
method: ActionMethod.updateConfig,
|
|
||||||
data: json.encode(updateConfigParams),
|
|
||||||
timeout: const Duration(seconds: 20),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> getProxies() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getProxies,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.changeProxy,
|
|
||||||
data: json.encode(changeProxyParams),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getExternalProviders() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getExternalProviders,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getExternalProvider,
|
|
||||||
data: externalProviderName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateGeoData({
|
|
||||||
required String geoType,
|
|
||||||
required String geoName,
|
|
||||||
}) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.updateGeoData,
|
|
||||||
data: json.encode(
|
|
||||||
{
|
|
||||||
"geoType": geoType,
|
|
||||||
"geoName": geoName,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> sideLoadExternalProvider({
|
|
||||||
required String providerName,
|
|
||||||
required String data,
|
|
||||||
}) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.sideLoadExternalProvider,
|
|
||||||
data: json.encode({
|
|
||||||
"providerName": providerName,
|
|
||||||
"data": data,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateExternalProvider(String providerName) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.updateExternalProvider,
|
|
||||||
data: providerName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getConnections() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getConnections,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> closeConnections() {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.closeConnections,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> closeConnection(String id) {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.closeConnection,
|
|
||||||
data: id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getTotalTraffic(bool value) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getTotalTraffic,
|
|
||||||
data: value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getTraffic(bool value) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getTraffic,
|
|
||||||
data: value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
resetTraffic() {
|
|
||||||
_prueInvoke(method: ActionMethod.resetTraffic);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
startLog() {
|
|
||||||
_prueInvoke(method: ActionMethod.startLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopLog() {
|
|
||||||
_prueInvoke(method: ActionMethod.stopLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> startListener() {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.startListener,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopListener() {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.stopListener,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> asyncTestDelay(String proxyName) {
|
|
||||||
final delayParams = {
|
|
||||||
"proxy-name": proxyName,
|
|
||||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
|
||||||
};
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.asyncTestDelay,
|
|
||||||
data: json.encode(delayParams),
|
|
||||||
timeout: Duration(
|
|
||||||
milliseconds: 6000,
|
|
||||||
),
|
|
||||||
onTimeout: () {
|
|
||||||
return json.encode(
|
|
||||||
Delay(
|
|
||||||
name: proxyName,
|
|
||||||
value: -1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() async {
|
|
||||||
final server = await serverCompleter.future;
|
|
||||||
await server.close();
|
|
||||||
await _deleteSocketFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getCountryCode(String ip) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getCountryCode,
|
|
||||||
data: ip,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getMemory() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getMemory,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension ColorExtension on Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color get toSoft {
|
Color get toSoft {
|
||||||
return withOpacity(0.12);
|
return withOpacity(0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color get toLittle {
|
Color get toLittle {
|
||||||
|
|||||||
@@ -34,3 +34,6 @@ export 'text.dart';
|
|||||||
export 'tray.dart';
|
export 'tray.dart';
|
||||||
export 'window.dart';
|
export 'window.dart';
|
||||||
export 'windows.dart';
|
export 'windows.dart';
|
||||||
|
export 'render.dart';
|
||||||
|
export 'mixin.dart';
|
||||||
|
export 'print.dart';
|
||||||
@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
|
|||||||
const appName = "FlClash";
|
const appName = "FlClash";
|
||||||
const appHelperService = "FlClashHelperService";
|
const appHelperService = "FlClashHelperService";
|
||||||
const coreName = "clash.meta";
|
const coreName = "clash.meta";
|
||||||
|
const browserUa =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||||
const packageName = "com.follow.clash";
|
const packageName = "com.follow.clash";
|
||||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
||||||
const helperPort = 47890;
|
const helperPort = 47890;
|
||||||
@@ -33,16 +35,6 @@ final double kHeaderHeight = system.isDesktop
|
|||||||
? 40
|
? 40
|
||||||
: 28
|
: 28
|
||||||
: 0;
|
: 0;
|
||||||
const GeoXMap defaultGeoXMap = {
|
|
||||||
"mmdb":
|
|
||||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
|
||||||
"asn":
|
|
||||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
|
|
||||||
"geoip":
|
|
||||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
|
|
||||||
"geosite":
|
|
||||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
|
||||||
};
|
|
||||||
const profilesDirectoryName = "profiles";
|
const profilesDirectoryName = "profiles";
|
||||||
const localhost = "127.0.0.1";
|
const localhost = "127.0.0.1";
|
||||||
const clashConfigKey = "clash_config";
|
const clashConfigKey = "clash_config";
|
||||||
@@ -53,8 +45,6 @@ const repository = "chen08209/FlClash";
|
|||||||
const defaultExternalController = "127.0.0.1:9090";
|
const defaultExternalController = "127.0.0.1:9090";
|
||||||
const maxMobileWidth = 600;
|
const maxMobileWidth = 600;
|
||||||
const maxLaptopWidth = 840;
|
const maxLaptopWidth = 840;
|
||||||
const geodataLoaderMemconservative = "memconservative";
|
|
||||||
const geodataLoaderStandard = "standard";
|
|
||||||
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
||||||
final filter = ImageFilter.blur(
|
final filter = ImageFilter.blur(
|
||||||
sigmaX: 5,
|
sigmaX: 5,
|
||||||
@@ -73,7 +63,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
|
|||||||
const stringAndStringMapEquality = MapEquality<String, String>();
|
const stringAndStringMapEquality = MapEquality<String, String>();
|
||||||
const stringAndStringMapEntryIterableEquality =
|
const stringAndStringMapEntryIterableEquality =
|
||||||
IterableEquality<MapEntry<String, String>>();
|
IterableEquality<MapEntry<String, String>>();
|
||||||
const stringAndIntQMapEquality = MapEquality<String, int?>();
|
const delayMapEquality = MapEquality<String, Map<String, int?>>();
|
||||||
const stringSetEquality = SetEquality<String>();
|
const stringSetEquality = SetEquality<String>();
|
||||||
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
|
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
|
||||||
|
|
||||||
@@ -88,3 +78,7 @@ const defaultPrimaryColor = Colors.brown;
|
|||||||
double getWidgetHeight(num lines) {
|
double getWidgetHeight(num lines) {
|
||||||
return max(lines * 84 + (lines - 1) * 16, 0);
|
return max(lines * 84 + (lines - 1) * 16, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final mainIsolate = "FlClashMainIsolate";
|
||||||
|
|
||||||
|
final serviceIsolate = "FlClashServiceIsolate";
|
||||||
|
|||||||
@@ -22,4 +22,23 @@ extension BuildContextExtension on BuildContext {
|
|||||||
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
||||||
|
|
||||||
TextTheme get textTheme => Theme.of(this).textTheme;
|
TextTheme get textTheme => Theme.of(this).textTheme;
|
||||||
|
|
||||||
|
T? findLastStateOfType<T extends State>() {
|
||||||
|
T? state;
|
||||||
|
|
||||||
|
visitor(Element element) {
|
||||||
|
if(!element.mounted){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(element is StatefulElement){
|
||||||
|
if (element.state is T) {
|
||||||
|
state = element.state as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.visitChildren(visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor(this as Element);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class Debouncer {
|
class Debouncer {
|
||||||
Map<dynamic, Timer> operators = {};
|
final Map<dynamic, Timer> _operations = {};
|
||||||
|
|
||||||
call(
|
call(
|
||||||
dynamic tag,
|
dynamic tag,
|
||||||
@@ -9,14 +9,15 @@ class Debouncer {
|
|||||||
List<dynamic>? args,
|
List<dynamic>? args,
|
||||||
Duration duration = const Duration(milliseconds: 600),
|
Duration duration = const Duration(milliseconds: 600),
|
||||||
}) {
|
}) {
|
||||||
final timer = operators[tag];
|
final timer = _operations[tag];
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
operators[tag] = Timer(
|
_operations[tag] = Timer(
|
||||||
duration,
|
duration,
|
||||||
() {
|
() {
|
||||||
operators.remove(tag);
|
_operations[tag]?.cancel();
|
||||||
|
_operations.remove(tag);
|
||||||
Function.apply(
|
Function.apply(
|
||||||
func,
|
func,
|
||||||
args,
|
args,
|
||||||
@@ -26,8 +27,59 @@ class Debouncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel(dynamic tag) {
|
cancel(dynamic tag) {
|
||||||
operators[tag]?.cancel();
|
_operations[tag]?.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Throttler {
|
||||||
|
final Map<dynamic, Timer> _operations = {};
|
||||||
|
|
||||||
|
call(
|
||||||
|
String tag,
|
||||||
|
Function func, {
|
||||||
|
List<dynamic>? args,
|
||||||
|
Duration duration = const Duration(milliseconds: 600),
|
||||||
|
}) {
|
||||||
|
final timer = _operations[tag];
|
||||||
|
if (timer != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_operations[tag] = Timer(
|
||||||
|
duration,
|
||||||
|
() {
|
||||||
|
_operations[tag]?.cancel();
|
||||||
|
_operations.remove(tag);
|
||||||
|
Function.apply(
|
||||||
|
func,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(dynamic tag) {
|
||||||
|
_operations[tag]?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> retry<T>({
|
||||||
|
required Future<T> Function() task,
|
||||||
|
int maxAttempts = 3,
|
||||||
|
required bool Function(T res) retryIf,
|
||||||
|
Duration delay = Duration.zero,
|
||||||
|
}) async {
|
||||||
|
int attempts = 0;
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
final res = await task();
|
||||||
|
if (!retryIf(res) || attempts >= maxAttempts) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
throw "unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
final debouncer = Debouncer();
|
final debouncer = Debouncer();
|
||||||
|
|
||||||
|
final throttler = Throttler();
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
|
|||||||
FutureOr<T> Function()? onTimeout,
|
FutureOr<T> Function()? onTimeout,
|
||||||
required String functionName,
|
required String functionName,
|
||||||
}) {
|
}) {
|
||||||
final realTimeout = timeout ?? const Duration(minutes: 1);
|
final realTimeout = timeout ?? const Duration(seconds: 30);
|
||||||
Timer(realTimeout + moreDuration, () {
|
Timer(realTimeout + commonDuration, () {
|
||||||
if (onLast != null) {
|
if (onLast != null) {
|
||||||
onLast();
|
onLast();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'constant.dart';
|
|
||||||
|
|
||||||
class FlClashHttpOverrides extends HttpOverrides {
|
class FlClashHttpOverrides extends HttpOverrides {
|
||||||
@override
|
@override
|
||||||
@@ -14,10 +13,9 @@ class FlClashHttpOverrides extends HttpOverrides {
|
|||||||
if ([localhost].contains(url.host)) {
|
if ([localhost].contains(url.host)) {
|
||||||
return "DIRECT";
|
return "DIRECT";
|
||||||
}
|
}
|
||||||
final appController = globalState.appController;
|
final port = globalState.config.patchClashConfig.mixedPort;
|
||||||
final port = appController.clashConfig.mixedPort;
|
final isStart = globalState.appState.runTime != null;
|
||||||
final isStart = appController.appFlowingState.isStart;
|
commonPrint.log("find $url proxy:$isStart");
|
||||||
debugPrint("find $url proxy:$isStart");
|
|
||||||
if (!isStart) return "DIRECT";
|
if (!isStart) return "DIRECT";
|
||||||
return "PROXY localhost:$port";
|
return "PROXY localhost:$port";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
import 'print.dart';
|
||||||
|
|
||||||
typedef InstallConfigCallBack = void Function(String url);
|
typedef InstallConfigCallBack = void Function(String url);
|
||||||
|
|
||||||
@@ -15,11 +16,11 @@ class LinkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initAppLinksListen(installConfigCallBack) async {
|
initAppLinksListen(installConfigCallBack) async {
|
||||||
debugPrint("initAppLinksListen");
|
commonPrint.log("initAppLinksListen");
|
||||||
destroy();
|
destroy();
|
||||||
subscription = _appLinks.uriLinkStream.listen(
|
subscription = _appLinks.uriLinkStream.listen(
|
||||||
(uri) {
|
(uri) {
|
||||||
debugPrint('onAppLink: $uri');
|
commonPrint.log('onAppLink: $uri');
|
||||||
if (uri.host == 'install-config') {
|
if (uri.host == 'install-config') {
|
||||||
final parameters = uri.queryParameters;
|
final parameters = uri.queryParameters;
|
||||||
final url = parameters['url'];
|
final url = parameters['url'];
|
||||||
|
|||||||
@@ -1,3 +1,66 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
class FixedList<T> {
|
||||||
|
final int maxLength;
|
||||||
|
final List<T> _list;
|
||||||
|
|
||||||
|
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
|
||||||
|
|
||||||
|
add(T item) {
|
||||||
|
if (_list.length == maxLength) {
|
||||||
|
_list.removeAt(0);
|
||||||
|
}
|
||||||
|
_list.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
_list.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> get list => List.unmodifiable(_list);
|
||||||
|
|
||||||
|
int get length => _list.length;
|
||||||
|
|
||||||
|
T operator [](int index) => _list[index];
|
||||||
|
|
||||||
|
FixedList<T> copyWith() {
|
||||||
|
return FixedList(
|
||||||
|
maxLength,
|
||||||
|
list: _list,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FixedMap<K, V> {
|
||||||
|
final int maxSize;
|
||||||
|
final Map<K, V> _map = {};
|
||||||
|
final Queue<K> _queue = Queue<K>();
|
||||||
|
|
||||||
|
FixedMap(this.maxSize);
|
||||||
|
|
||||||
|
put(K key, V value) {
|
||||||
|
if (_map.length == maxSize) {
|
||||||
|
final oldestKey = _queue.removeFirst();
|
||||||
|
_map.remove(oldestKey);
|
||||||
|
}
|
||||||
|
_map[key] = value;
|
||||||
|
_queue.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
_map.clear();
|
||||||
|
_queue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
V? get(K key) => _map[key];
|
||||||
|
|
||||||
|
bool containsKey(K key) => _map.containsKey(key);
|
||||||
|
|
||||||
|
int get length => _map.length;
|
||||||
|
|
||||||
|
Map<K, V> get map => Map.unmodifiable(_map);
|
||||||
|
}
|
||||||
|
|
||||||
extension ListExtension<T> on List<T> {
|
extension ListExtension<T> on List<T> {
|
||||||
List<T> intersection(List<T> list) {
|
List<T> intersection(List<T> list) {
|
||||||
return where((item) => list.contains(item)).toList();
|
return where((item) => list.contains(item)).toList();
|
||||||
@@ -17,8 +80,8 @@ extension ListExtension<T> on List<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<T> safeSublist(int start) {
|
List<T> safeSublist(int start) {
|
||||||
if(start <= 0) return this;
|
if (start <= 0) return this;
|
||||||
if(start > length) return [];
|
if (start > length) return [];
|
||||||
return sublist(start);
|
return sublist(start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class SingleInstanceLock {
|
|||||||
|
|
||||||
Future<bool> acquire() async {
|
Future<bool> acquire() async {
|
||||||
try {
|
try {
|
||||||
final lockFilePath = await appPath.getLockFilePath();
|
final lockFilePath = await appPath.lockFilePath;
|
||||||
final lockFile = File(lockFilePath);
|
final lockFile = File(lockFilePath);
|
||||||
await lockFile.create();
|
await lockFile.create();
|
||||||
_accessFile = await lockFile.open(mode: FileMode.write);
|
_accessFile = await lockFile.open(mode: FileMode.write);
|
||||||
|
|||||||
@@ -4,20 +4,32 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class Measure {
|
class Measure {
|
||||||
final TextScaler _textScale;
|
final TextScaler _textScale;
|
||||||
late BuildContext context;
|
final BuildContext context;
|
||||||
|
final String? _fontFamily;
|
||||||
|
|
||||||
Measure.of(this.context)
|
Measure.of(this.context, {String? fontFamily})
|
||||||
: _textScale = TextScaler.linear(
|
: _textScale = TextScaler.linear(
|
||||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||||
);
|
),
|
||||||
|
_fontFamily = fontFamily ?? "";
|
||||||
|
|
||||||
Size computeTextSize(Text text) {
|
Size computeTextSize(
|
||||||
|
Text text, {
|
||||||
|
double maxWidth = double.infinity,
|
||||||
|
}) {
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
text: TextSpan(text: text.data, style: text.style),
|
text: TextSpan(
|
||||||
|
text: text.data,
|
||||||
|
style: text.style?.copyWith(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
),
|
||||||
|
),
|
||||||
maxLines: text.maxLines,
|
maxLines: text.maxLines,
|
||||||
textScaler: _textScale,
|
textScaler: _textScale,
|
||||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||||
)..layout();
|
)..layout(
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
);
|
||||||
return textPainter.size;
|
return textPainter.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
lib/common/mixin.dart
Normal file
46
lib/common/mixin.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'context.dart';
|
||||||
|
|
||||||
|
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
|
||||||
|
set value(T value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(previous, next) {
|
||||||
|
final res = super.updateShouldNotify(previous, next);
|
||||||
|
if (res) {
|
||||||
|
onUpdate(next);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(T value) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||||
|
void onPageShow() {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
initPageState() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
commonScaffoldState?.actions = actions;
|
||||||
|
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||||
|
commonScaffoldState?.onSearch = onSearch;
|
||||||
|
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPageHidden() {}
|
||||||
|
|
||||||
|
List<Widget> get actions => [];
|
||||||
|
|
||||||
|
Widget? get floatingActionButton => null;
|
||||||
|
|
||||||
|
Function(String)? get onSearch => null;
|
||||||
|
|
||||||
|
Function(List<String>)? get onKeywordsUpdate => null;
|
||||||
|
}
|
||||||
@@ -6,55 +6,81 @@ import 'package:flutter/material.dart';
|
|||||||
class Navigation {
|
class Navigation {
|
||||||
static Navigation? _instance;
|
static Navigation? _instance;
|
||||||
|
|
||||||
getItems({
|
List<NavigationItem> getItems({
|
||||||
bool openLogs = false,
|
bool openLogs = false,
|
||||||
bool hasProxies = false,
|
bool hasProxies = false,
|
||||||
}) {
|
}) {
|
||||||
return [
|
return [
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.space_dashboard),
|
icon: Icon(Icons.space_dashboard),
|
||||||
label: "dashboard",
|
label: PageLabel.dashboard,
|
||||||
fragment: DashboardFragment(),
|
fragment: DashboardFragment(
|
||||||
|
key: GlobalObjectKey(PageLabel.dashboard),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
icon: const Icon(Icons.rocket),
|
icon: const Icon(Icons.article),
|
||||||
label: "proxies",
|
label: PageLabel.proxies,
|
||||||
fragment: const ProxiesFragment(),
|
fragment: const ProxiesFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.proxies,
|
||||||
|
),
|
||||||
|
),
|
||||||
modes: hasProxies
|
modes: hasProxies
|
||||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.folder),
|
icon: Icon(Icons.folder),
|
||||||
label: "profiles",
|
label: PageLabel.profiles,
|
||||||
fragment: ProfilesFragment(),
|
fragment: ProfilesFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.profiles,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.view_timeline),
|
icon: Icon(Icons.view_timeline),
|
||||||
label: "requests",
|
label: PageLabel.requests,
|
||||||
fragment: RequestsFragment(),
|
fragment: RequestsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.requests,
|
||||||
|
),
|
||||||
|
),
|
||||||
description: "requestsDesc",
|
description: "requestsDesc",
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.ballot),
|
icon: Icon(Icons.ballot),
|
||||||
label: "connections",
|
label: PageLabel.connections,
|
||||||
fragment: ConnectionsFragment(),
|
fragment: ConnectionsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.connections,
|
||||||
|
),
|
||||||
|
),
|
||||||
description: "connectionsDesc",
|
description: "connectionsDesc",
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.storage),
|
icon: Icon(Icons.storage),
|
||||||
label: "resources",
|
label: PageLabel.resources,
|
||||||
description: "resourcesDesc",
|
description: "resourcesDesc",
|
||||||
keep: false,
|
keep: false,
|
||||||
fragment: Resources(),
|
fragment: Resources(
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.resources,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
modes: [NavigationItemMode.more],
|
||||||
),
|
),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
icon: const Icon(Icons.adb),
|
icon: const Icon(Icons.adb),
|
||||||
label: "logs",
|
label: PageLabel.logs,
|
||||||
fragment: const LogsFragment(),
|
fragment: const LogsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.logs,
|
||||||
|
),
|
||||||
|
),
|
||||||
description: "logsDesc",
|
description: "logsDesc",
|
||||||
modes: openLogs
|
modes: openLogs
|
||||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||||
@@ -62,8 +88,12 @@ class Navigation {
|
|||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.construction),
|
icon: Icon(Icons.construction),
|
||||||
label: "tools",
|
label: PageLabel.tools,
|
||||||
fragment: ToolsFragment(),
|
fragment: ToolsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.tools,
|
||||||
|
),
|
||||||
|
),
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class BaseNavigator {
|
class BaseNavigator {
|
||||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||||
|
if (globalState.appState.viewMode != ViewMode.mobile) {
|
||||||
|
return await Navigator.of(context).push<T>(
|
||||||
|
CommonDesktopRoute(
|
||||||
|
builder: (context) => child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return await Navigator.of(context).push<T>(
|
return await Navigator.of(context).push<T>(
|
||||||
CommonRoute(
|
CommonRoute(
|
||||||
builder: (context) => child,
|
builder: (context) => child,
|
||||||
@@ -11,6 +21,46 @@ class BaseNavigator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CommonDesktopRoute<T> extends PageRoute<T> {
|
||||||
|
final Widget Function(BuildContext context) builder;
|
||||||
|
|
||||||
|
CommonDesktopRoute({
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
final Widget result = builder(context);
|
||||||
|
return Semantics(
|
||||||
|
scopesRoute: true,
|
||||||
|
explicitChildNodes: true,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: result,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get reverseTransitionDuration => Duration(milliseconds: 200);
|
||||||
|
}
|
||||||
|
|
||||||
class CommonRoute<T> extends MaterialPageRoute<T> {
|
class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||||
CommonRoute({
|
CommonRoute({
|
||||||
required super.builder,
|
required super.builder,
|
||||||
@@ -20,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
|
|||||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
|
Duration get reverseTransitionDuration => const Duration(milliseconds: 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
||||||
@@ -224,7 +274,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final double shadowWidth = 0.05 * configuration.size!.width;
|
final double shadowWidth = 0.03 * configuration.size!.width;
|
||||||
final double shadowHeight = configuration.size!.height;
|
final double shadowHeight = configuration.size!.height;
|
||||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
final double bandWidth = shadowWidth / (colors.length - 1);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension NumExt on num {
|
extension NumExt on num {
|
||||||
String fixed({digit = 2}) {
|
String fixed({decimals = 2}) {
|
||||||
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
String formatted = toStringAsFixed(decimals);
|
||||||
|
if (formatted.contains('.')) {
|
||||||
|
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
|
||||||
|
if (formatted.endsWith('.')) {
|
||||||
|
formatted = formatted.substring(0, formatted.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:lpinyin/lpinyin.dart';
|
import 'package:lpinyin/lpinyin.dart';
|
||||||
import 'package:zxing2/qrcode.dart';
|
|
||||||
|
|
||||||
class Other {
|
class Other {
|
||||||
Color? getDelayColor(int? delay) {
|
Color? getDelayColor(int? delay) {
|
||||||
@@ -34,6 +30,26 @@ class Other {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String generateRandomString({int minLength = 10, int maxLength = 100}) {
|
||||||
|
const latinChars =
|
||||||
|
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
final random = Random();
|
||||||
|
|
||||||
|
int length = minLength + random.nextInt(maxLength - minLength + 1);
|
||||||
|
|
||||||
|
String result = '';
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
if (random.nextBool()) {
|
||||||
|
result +=
|
||||||
|
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
|
||||||
|
} else {
|
||||||
|
result += latinChars[random.nextInt(latinChars.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
String get uuidV4 {
|
String get uuidV4 {
|
||||||
final Random random = Random();
|
final Random random = Random();
|
||||||
final bytes = List.generate(16, (_) => random.nextInt(256));
|
final bytes = List.generate(16, (_) => random.nextInt(256));
|
||||||
@@ -165,30 +181,6 @@ class Other {
|
|||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> parseQRCode(Uint8List? bytes) {
|
|
||||||
return Isolate.run<String?>(() {
|
|
||||||
if (bytes == null) return null;
|
|
||||||
img.Image? image = img.decodeImage(bytes);
|
|
||||||
LuminanceSource source = RGBLuminanceSource(
|
|
||||||
image!.width,
|
|
||||||
image.height,
|
|
||||||
image
|
|
||||||
.convert(numChannels: 4)
|
|
||||||
.getBytes(order: img.ChannelOrder.abgr)
|
|
||||||
.buffer
|
|
||||||
.asInt32List(),
|
|
||||||
);
|
|
||||||
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
|
|
||||||
final reader = QRCodeReader();
|
|
||||||
try {
|
|
||||||
final result = reader.decode(bitmap);
|
|
||||||
return result.text;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String? getFileNameForDisposition(String? disposition) {
|
String? getFileNameForDisposition(String? disposition) {
|
||||||
if (disposition == null) return null;
|
if (disposition == null) return null;
|
||||||
final parseValue = HeaderValue.parse(disposition);
|
final parseValue = HeaderValue.parse(disposition);
|
||||||
|
|||||||
@@ -48,35 +48,40 @@ class AppPath {
|
|||||||
return join(executableDirPath, "$appHelperService$executableExtension");
|
return join(executableDirPath, "$appHelperService$executableExtension");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getDownloadDirPath() async {
|
Future<String> get downloadDirPath async {
|
||||||
final directory = await downloadDir.future;
|
final directory = await downloadDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getHomeDirPath() async {
|
Future<String> get homeDirPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getLockFilePath() async {
|
Future<String> get lockFilePath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return join(directory.path, "FlClash.lock");
|
return join(directory.path, "FlClash.lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getProfilesPath() async {
|
Future<String> get sharedPreferencesPath async {
|
||||||
|
final directory = await dataDir.future;
|
||||||
|
return join(directory.path, "shared_preferences.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> get profilesPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return join(directory.path, profilesDirectoryName);
|
return join(directory.path, profilesDirectoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getProfilePath(String? id) async {
|
Future<String?> getProfilePath(String? id) async {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
final directory = await getProfilesPath();
|
final directory = await profilesPath;
|
||||||
return join(directory, "$id.yaml");
|
return join(directory, "$id.yaml");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getProvidersPath(String? id) async {
|
Future<String?> getProvidersPath(String? id) async {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
final directory = await getProfilesPath();
|
final directory = await profilesPath;
|
||||||
return join(directory, "providers", id);
|
return join(directory, "providers", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import 'dart:typed_data';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
class Picker {
|
class Picker {
|
||||||
Future<PlatformFile?> pickerFile() async {
|
Future<PlatformFile?> pickerFile() async {
|
||||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||||
withData: true,
|
withData: true,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
initialDirectory: await appPath.getDownloadDirPath(),
|
initialDirectory: await appPath.downloadDirPath,
|
||||||
);
|
);
|
||||||
return filePickerResult?.files.first;
|
return filePickerResult?.files.first;
|
||||||
}
|
}
|
||||||
@@ -18,7 +19,7 @@ class Picker {
|
|||||||
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
||||||
final path = await FilePicker.platform.saveFile(
|
final path = await FilePicker.platform.saveFile(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
initialDirectory: await appPath.getDownloadDirPath(),
|
initialDirectory: await appPath.downloadDirPath,
|
||||||
bytes: Platform.isAndroid ? bytes : null,
|
bytes: Platform.isAndroid ? bytes : null,
|
||||||
);
|
);
|
||||||
if (!Platform.isAndroid && path != null) {
|
if (!Platform.isAndroid && path != null) {
|
||||||
@@ -30,9 +31,14 @@ class Picker {
|
|||||||
|
|
||||||
Future<String?> pickerConfigQRCode() async {
|
Future<String?> pickerConfigQRCode() async {
|
||||||
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
final bytes = await xFile?.readAsBytes();
|
if (xFile == null) {
|
||||||
if (bytes == null) return null;
|
return null;
|
||||||
final result = await other.parseQRCode(bytes);
|
}
|
||||||
|
final controller = MobileScannerController();
|
||||||
|
final capture = await controller.analyzeImage(xFile.path, formats: [
|
||||||
|
BarcodeFormat.qrCode,
|
||||||
|
]);
|
||||||
|
final result = capture?.barcodes.first.rawValue;
|
||||||
if (result == null || !result.isUrl) {
|
if (result == null || !result.isUrl) {
|
||||||
throw appLocalizations.pleaseUploadValidQrcode;
|
throw appLocalizations.pleaseUploadValidQrcode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
|
||||||
import 'constant.dart';
|
import 'constant.dart';
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
static Preferences? _instance;
|
static Preferences? _instance;
|
||||||
Completer<SharedPreferences> sharedPreferencesCompleter = Completer();
|
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
||||||
|
|
||||||
|
Future<bool> get isInit async =>
|
||||||
|
await sharedPreferencesCompleter.future != null;
|
||||||
|
|
||||||
Preferences._internal() {
|
Preferences._internal() {
|
||||||
SharedPreferences.getInstance()
|
SharedPreferences.getInstance()
|
||||||
.then((value) => sharedPreferencesCompleter.complete(value));
|
.then((value) => sharedPreferencesCompleter.complete(value))
|
||||||
|
.onError((_, __) => sharedPreferencesCompleter.complete(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Preferences() {
|
factory Preferences() {
|
||||||
@@ -23,50 +26,38 @@ class Preferences {
|
|||||||
|
|
||||||
Future<ClashConfig?> getClashConfig() async {
|
Future<ClashConfig?> getClashConfig() async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
final clashConfigString = preferences.getString(clashConfigKey);
|
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||||
if (clashConfigString == null) return null;
|
if (clashConfigString == null) return null;
|
||||||
final clashConfigMap = json.decode(clashConfigString);
|
final clashConfigMap = json.decode(clashConfigString);
|
||||||
try {
|
return ClashConfig.fromJson(clashConfigMap);
|
||||||
return ClashConfig.fromJson(clashConfigMap);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(e.toString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
|
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
|
||||||
return preferences.setString(
|
|
||||||
clashConfigKey,
|
|
||||||
json.encode(clashConfig),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Config?> getConfig() async {
|
Future<Config?> getConfig() async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
final configString = preferences.getString(configKey);
|
final configString = preferences?.getString(configKey);
|
||||||
if (configString == null) return null;
|
if (configString == null) return null;
|
||||||
final configMap = json.decode(configString);
|
final configMap = json.decode(configString);
|
||||||
try {
|
return Config.compatibleFromJson(configMap);
|
||||||
return Config.fromJson(configMap);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(e.toString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveConfig(Config config) async {
|
Future<bool> saveConfig(Config config) async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
return preferences.setString(
|
return await preferences?.setString(
|
||||||
configKey,
|
configKey,
|
||||||
json.encode(config),
|
json.encode(config),
|
||||||
);
|
) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearClashConfig() async {
|
||||||
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
|
preferences?.remove(clashConfigKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPreferences() async {
|
clearPreferences() async {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
||||||
sharedPreferencesIns.clear();
|
sharedPreferencesIns?.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final preferences = Preferences();
|
final preferences = Preferences();
|
||||||
|
|||||||
31
lib/common/print.dart
Normal file
31
lib/common/print.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class CommonPrint {
|
||||||
|
static CommonPrint? _instance;
|
||||||
|
|
||||||
|
CommonPrint._internal();
|
||||||
|
|
||||||
|
factory CommonPrint() {
|
||||||
|
_instance ??= CommonPrint._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(String? text) {
|
||||||
|
final payload = "[FlClash] $text";
|
||||||
|
debugPrint(payload);
|
||||||
|
if (globalState.isService) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalState.appController.addLog(
|
||||||
|
Log(
|
||||||
|
logLevel: LogLevel.info,
|
||||||
|
payload: payload,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final commonPrint = CommonPrint();
|
||||||
57
lib/common/render.dart
Normal file
57
lib/common/render.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.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(
|
||||||
|
DebounceTag.renderPause,
|
||||||
|
_pause,
|
||||||
|
duration: Duration(seconds: 15),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
debouncer.cancel(DebounceTag.renderPause);
|
||||||
|
_resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pause() {
|
||||||
|
if (_isPaused) return;
|
||||||
|
_isPaused = true;
|
||||||
|
_beginFrame = _dispatcher.onBeginFrame;
|
||||||
|
_drawFrame = _dispatcher.onDrawFrame;
|
||||||
|
_dispatcher.onBeginFrame = null;
|
||||||
|
_dispatcher.onDrawFrame = null;
|
||||||
|
commonPrint.log("pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resume() {
|
||||||
|
if (!_isPaused) return;
|
||||||
|
_isPaused = false;
|
||||||
|
_dispatcher.onBeginFrame = _beginFrame;
|
||||||
|
_dispatcher.onDrawFrame = _drawFrame;
|
||||||
|
_dispatcher.scheduleFrame();
|
||||||
|
commonPrint.log("resume");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final render = system.isDesktop ? Render() : null;
|
||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
@@ -14,11 +13,10 @@ class Request {
|
|||||||
String? userAgent;
|
String? userAgent;
|
||||||
|
|
||||||
Request() {
|
Request() {
|
||||||
_dio = Dio();
|
_dio = Dio(
|
||||||
_dio.interceptors.add(
|
BaseOptions(
|
||||||
InterceptorsWrapper(
|
headers: {
|
||||||
onRequest: (options, handler) {
|
"User-Agent": browserUa,
|
||||||
return handler.next(options); // 继续请求
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -30,7 +28,7 @@ class Request {
|
|||||||
url,
|
url,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
"User-Agent": globalState.ua,
|
||||||
},
|
},
|
||||||
responseType: ResponseType.bytes,
|
responseType: ResponseType.bytes,
|
||||||
),
|
),
|
||||||
@@ -71,31 +69,32 @@ class Request {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> _ipInfoSources = [
|
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||||
"https://ipwho.is/?fields=ip&output=csv",
|
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||||
"https://ipinfo.io/ip",
|
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||||
"https://ifconfig.me/ip/",
|
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||||
];
|
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||||
|
};
|
||||||
|
|
||||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||||
for (final source in _ipInfoSources) {
|
for (final source in _ipInfoSources.entries) {
|
||||||
try {
|
try {
|
||||||
final response = await _dio
|
final response = await _dio.get<Map<String, dynamic>>(
|
||||||
.get<String>(
|
source.key,
|
||||||
source,
|
cancelToken: cancelToken,
|
||||||
cancelToken: cancelToken,
|
options: Options(
|
||||||
)
|
responseType: ResponseType.json,
|
||||||
.timeout(httpTimeoutDuration);
|
),
|
||||||
|
);
|
||||||
if (response.statusCode != 200 || response.data == null) {
|
if (response.statusCode != 200 || response.data == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
if (response.data == null) {
|
||||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return ipInfo;
|
return source.value(response.data!);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("checkIp error ===> $e");
|
commonPrint.log("checkIp error ===> $e");
|
||||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||||
throw "cancelled";
|
throw "cancelled";
|
||||||
}
|
}
|
||||||
@@ -110,6 +109,9 @@ class Request {
|
|||||||
.get(
|
.get(
|
||||||
"http://$localhost:$helperPort/ping",
|
"http://$localhost:$helperPort/ping",
|
||||||
options: Options(
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"User-Agent": browserUa,
|
||||||
|
},
|
||||||
responseType: ResponseType.plain,
|
responseType: ResponseType.plain,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BaseScrollBehavior2 extends ScrollBehavior {}
|
||||||
|
|
||||||
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
||||||
@override
|
@override
|
||||||
Widget buildScrollbar(
|
Widget buildScrollbar(
|
||||||
@@ -40,3 +43,95 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NextClampingScrollPhysics extends ClampingScrollPhysics {
|
||||||
|
const NextClampingScrollPhysics({super.parent});
|
||||||
|
|
||||||
|
@override
|
||||||
|
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||||
|
return NextClampingScrollPhysics(parent: buildParent(ancestor));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Simulation? createBallisticSimulation(
|
||||||
|
ScrollMetrics position, double velocity) {
|
||||||
|
final Tolerance tolerance = toleranceFor(position);
|
||||||
|
if (position.outOfRange) {
|
||||||
|
double? end;
|
||||||
|
if (position.pixels > position.maxScrollExtent) {
|
||||||
|
end = position.maxScrollExtent;
|
||||||
|
}
|
||||||
|
if (position.pixels < position.minScrollExtent) {
|
||||||
|
end = position.minScrollExtent;
|
||||||
|
}
|
||||||
|
assert(end != null);
|
||||||
|
return ScrollSpringSimulation(
|
||||||
|
spring,
|
||||||
|
end!,
|
||||||
|
end,
|
||||||
|
min(0.0, velocity),
|
||||||
|
tolerance: tolerance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (velocity.abs() < tolerance.velocity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ClampingScrollSimulation(
|
||||||
|
position: position.pixels,
|
||||||
|
velocity: velocity,
|
||||||
|
tolerance: tolerance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReverseScrollController extends ScrollController {
|
||||||
|
ReverseScrollController({
|
||||||
|
super.initialScrollOffset,
|
||||||
|
super.keepScrollOffset,
|
||||||
|
super.debugLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ScrollPosition createScrollPosition(
|
||||||
|
ScrollPhysics physics,
|
||||||
|
ScrollContext context,
|
||||||
|
ScrollPosition? oldPosition,
|
||||||
|
) {
|
||||||
|
return ReverseScrollPosition(
|
||||||
|
physics: physics,
|
||||||
|
context: context,
|
||||||
|
initialPixels: initialScrollOffset,
|
||||||
|
keepScrollOffset: keepScrollOffset,
|
||||||
|
oldPosition: oldPosition,
|
||||||
|
debugLabel: debugLabel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
|
||||||
|
ReverseScrollPosition({
|
||||||
|
required super.physics,
|
||||||
|
required super.context,
|
||||||
|
super.initialPixels = 0.0,
|
||||||
|
super.keepScrollOffset,
|
||||||
|
super.oldPosition,
|
||||||
|
super.debugLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool _isInit = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
|
||||||
|
if (!_isInit) {
|
||||||
|
correctPixels(maxScrollExtent);
|
||||||
|
_isInit = true;
|
||||||
|
}
|
||||||
|
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
0
lib/common/state.dart
Normal file
0
lib/common/state.dart
Normal file
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'print.dart';
|
||||||
|
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
bool get isUrl {
|
bool get isUrl {
|
||||||
@@ -43,8 +43,17 @@ extension StringExtension on String {
|
|||||||
RegExp(this);
|
RegExp(this);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(e.toString());
|
commonPrint.log(e.toString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StringExtensionSafe on String? {
|
||||||
|
String getSafeValue(String defaultValue) {
|
||||||
|
if (this == null || this!.isEmpty) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return this!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class System {
|
|||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
|
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
|
||||||
final output = result.stdout.trim();
|
final output = result.stdout.trim();
|
||||||
if (output.startsWith('root:') && output.contains('rwx')) {
|
if (output.startsWith('root:') && output.contains('rws')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ class Tray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update({
|
update({
|
||||||
required AppState appState,
|
required TrayState trayState,
|
||||||
required AppFlowingState appFlowingState,
|
|
||||||
required Config config,
|
|
||||||
required ClashConfig clashConfig,
|
|
||||||
bool focus = false,
|
bool focus = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -50,7 +47,7 @@ class Tray {
|
|||||||
}
|
}
|
||||||
if (!Platform.isLinux) {
|
if (!Platform.isLinux) {
|
||||||
await _updateSystemTray(
|
await _updateSystemTray(
|
||||||
brightness: appState.brightness,
|
brightness: trayState.brightness,
|
||||||
force: focus,
|
force: focus,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,9 +60,7 @@ class Tray {
|
|||||||
);
|
);
|
||||||
menuItems.add(showMenuItem);
|
menuItems.add(showMenuItem);
|
||||||
final startMenuItem = MenuItem.checkbox(
|
final startMenuItem = MenuItem.checkbox(
|
||||||
label: appFlowingState.isStart
|
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||||
? appLocalizations.stop
|
|
||||||
: appLocalizations.start,
|
|
||||||
onClick: (_) async {
|
onClick: (_) async {
|
||||||
globalState.appController.updateStart();
|
globalState.appController.updateStart();
|
||||||
},
|
},
|
||||||
@@ -80,23 +75,22 @@ class Tray {
|
|||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
globalState.appController.changeMode(mode);
|
globalState.appController.changeMode(mode);
|
||||||
},
|
},
|
||||||
checked: mode == clashConfig.mode,
|
checked: mode == trayState.mode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
menuItems.add(MenuItem.separator());
|
menuItems.add(MenuItem.separator());
|
||||||
if (!Platform.isWindows) {
|
if (!Platform.isWindows) {
|
||||||
final groups = appState.currentGroups;
|
for (final group in trayState.groups) {
|
||||||
for (final group in groups) {
|
|
||||||
List<MenuItem> subMenuItems = [];
|
List<MenuItem> subMenuItems = [];
|
||||||
for (final proxy in group.all) {
|
for (final proxy in group.all) {
|
||||||
subMenuItems.add(
|
subMenuItems.add(
|
||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
label: proxy.name,
|
label: proxy.name,
|
||||||
checked: appState.selectedMap[group.name] == proxy.name,
|
checked: trayState.selectedMap[group.name] == proxy.name,
|
||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
appController.config.updateCurrentSelectedMap(
|
appController.updateCurrentSelectedMap(
|
||||||
group.name,
|
group.name,
|
||||||
proxy.name,
|
proxy.name,
|
||||||
);
|
);
|
||||||
@@ -117,18 +111,18 @@ class Tray {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (groups.isNotEmpty) {
|
if (trayState.groups.isNotEmpty) {
|
||||||
menuItems.add(MenuItem.separator());
|
menuItems.add(MenuItem.separator());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appFlowingState.isStart) {
|
if (trayState.isStart) {
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
label: appLocalizations.tun,
|
label: appLocalizations.tun,
|
||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
globalState.appController.updateTun();
|
globalState.appController.updateTun();
|
||||||
},
|
},
|
||||||
checked: clashConfig.tun.enable,
|
checked: trayState.tunEnable,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
@@ -137,7 +131,7 @@ class Tray {
|
|||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
globalState.appController.updateSystemProxy();
|
globalState.appController.updateSystemProxy();
|
||||||
},
|
},
|
||||||
checked: config.networkProps.systemProxy,
|
checked: trayState.systemProxy,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
menuItems.add(MenuItem.separator());
|
menuItems.add(MenuItem.separator());
|
||||||
@@ -147,12 +141,12 @@ class Tray {
|
|||||||
onClick: (_) async {
|
onClick: (_) async {
|
||||||
globalState.appController.updateAutoLaunch();
|
globalState.appController.updateAutoLaunch();
|
||||||
},
|
},
|
||||||
checked: config.appSetting.autoLaunch,
|
checked: trayState.autoLaunch,
|
||||||
);
|
);
|
||||||
final copyEnvVarMenuItem = MenuItem(
|
final copyEnvVarMenuItem = MenuItem(
|
||||||
label: appLocalizations.copyEnvVar,
|
label: appLocalizations.copyEnvVar,
|
||||||
onClick: (_) async {
|
onClick: (_) async {
|
||||||
await _copyEnv(clashConfig.mixedPort);
|
await _copyEnv(trayState.port);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
menuItems.add(autoStartMenuItem);
|
menuItems.add(autoStartMenuItem);
|
||||||
@@ -169,12 +163,25 @@ class Tray {
|
|||||||
await trayManager.setContextMenu(menu);
|
await trayManager.setContextMenu(menu);
|
||||||
if (Platform.isLinux) {
|
if (Platform.isLinux) {
|
||||||
await _updateSystemTray(
|
await _updateSystemTray(
|
||||||
brightness: appState.brightness,
|
brightness: trayState.brightness,
|
||||||
force: focus,
|
force: focus,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTrayTitle([Traffic? traffic]) async {
|
||||||
|
// if (!Platform.isMacOS) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (traffic == null) {
|
||||||
|
// await trayManager.setTitle("");
|
||||||
|
// } else {
|
||||||
|
// await trayManager.setTitle(
|
||||||
|
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _copyEnv(int port) async {
|
Future<void> _copyEnv(int port) async {
|
||||||
final url = "http://127.0.0.1:$port";
|
final url = "http://127.0.0.1:$port";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/config.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:screen_retriever/screen_retriever.dart';
|
import 'package:screen_retriever/screen_retriever.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class Window {
|
class Window {
|
||||||
init(WindowProps props, int version) async {
|
init(int version) async {
|
||||||
|
final props = globalState.config.windowProps;
|
||||||
final acquire = await singleInstanceLock.acquire();
|
final acquire = await singleInstanceLock.acquire();
|
||||||
if (!acquire) {
|
if (!acquire) {
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -24,6 +25,8 @@ class Window {
|
|||||||
);
|
);
|
||||||
if (!Platform.isMacOS || version > 10) {
|
if (!Platform.isMacOS || version > 10) {
|
||||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||||
|
}
|
||||||
|
if(!Platform.isMacOS){
|
||||||
final left = props.left ?? 0;
|
final left = props.left ?? 0;
|
||||||
final top = props.top ?? 0;
|
final top = props.top ?? 0;
|
||||||
final right = left + props.width;
|
final right = left + props.width;
|
||||||
@@ -33,7 +36,7 @@ class Window {
|
|||||||
} else {
|
} else {
|
||||||
final displays = await screenRetriever.getAllDisplays();
|
final displays = await screenRetriever.getAllDisplays();
|
||||||
final isPositionValid = displays.any(
|
final isPositionValid = displays.any(
|
||||||
(display) {
|
(display) {
|
||||||
final displayBounds = Rect.fromLTWH(
|
final displayBounds = Rect.fromLTWH(
|
||||||
display.visiblePosition!.dx,
|
display.visiblePosition!.dx,
|
||||||
display.visiblePosition!.dy,
|
display.visiblePosition!.dy,
|
||||||
@@ -60,6 +63,7 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() async {
|
show() async {
|
||||||
|
render?.resume();
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
await windowManager.focus();
|
||||||
await windowManager.setSkipTaskbar(false);
|
await windowManager.setSkipTaskbar(false);
|
||||||
@@ -74,6 +78,7 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hide() async {
|
hide() async {
|
||||||
|
render?.pause();
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
await windowManager.setSkipTaskbar(true);
|
await windowManager.setSkipTaskbar(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
|||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class Windows {
|
class Windows {
|
||||||
@@ -54,7 +53,7 @@ class Windows {
|
|||||||
calloc.free(argumentsPtr);
|
calloc.free(argumentsPtr);
|
||||||
calloc.free(operationPtr);
|
calloc.free(operationPtr);
|
||||||
|
|
||||||
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
|
commonPrint.log("windows runas: $command $arguments resultCode:$result");
|
||||||
|
|
||||||
if (result < 42) {
|
if (result < 42) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -8,28 +8,24 @@ import 'package:archive/archive.dart';
|
|||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/archive.dart';
|
import 'package:fl_clash/common/archive.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'common/common.dart';
|
import 'common/common.dart';
|
||||||
import 'models/models.dart';
|
import 'models/models.dart';
|
||||||
|
|
||||||
class AppController {
|
class AppController {
|
||||||
final BuildContext context;
|
bool lastTunEnable = false;
|
||||||
late AppState appState;
|
int? lastProfileModified;
|
||||||
late AppFlowingState appFlowingState;
|
|
||||||
late Config config;
|
|
||||||
late ClashConfig clashConfig;
|
|
||||||
|
|
||||||
AppController(this.context) {
|
final BuildContext context;
|
||||||
appState = context.read<AppState>();
|
final WidgetRef _ref;
|
||||||
config = context.read<Config>();
|
|
||||||
clashConfig = context.read<ClashConfig>();
|
AppController(this.context, WidgetRef ref) : _ref = ref;
|
||||||
appFlowingState = context.read<AppFlowingState>();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClashConfigDebounce() {
|
updateClashConfigDebounce() {
|
||||||
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
||||||
@@ -41,13 +37,13 @@ class AppController {
|
|||||||
|
|
||||||
addCheckIpNumDebounce() {
|
addCheckIpNumDebounce() {
|
||||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||||
appState.checkIpNum++;
|
_ref.read(checkIpNumProvider.notifier).add();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyProfileDebounce() {
|
applyProfileDebounce() {
|
||||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||||
applyProfile(isPrue: true);
|
applyProfile();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,30 +63,27 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restartCore() async {
|
restartCore() async {
|
||||||
await globalState.restartCore(
|
await clashService?.reStart();
|
||||||
appState: appState,
|
await initCore();
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
if (_ref.read(runTimeProvider.notifier).isStart) {
|
||||||
);
|
await globalState.handleStart();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(bool isStart) async {
|
updateStatus(bool isStart) async {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
await globalState.handleStart();
|
await globalState.handleStart([
|
||||||
updateRunTime();
|
|
||||||
updateTraffic();
|
|
||||||
globalState.updateFunctionLists = [
|
|
||||||
updateRunTime,
|
updateRunTime,
|
||||||
updateTraffic,
|
updateTraffic,
|
||||||
];
|
]);
|
||||||
final currentLastModified =
|
final currentLastModified =
|
||||||
await config.getCurrentProfile()?.profileLastModified;
|
await _ref.read(currentProfileProvider)?.profileLastModified;
|
||||||
if (currentLastModified == null ||
|
if (currentLastModified == null || lastProfileModified == null) {
|
||||||
globalState.lastProfileModified == null) {
|
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
|
if (currentLastModified <= (lastProfileModified ?? 0)) {
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -98,9 +91,10 @@ class AppController {
|
|||||||
} else {
|
} else {
|
||||||
await globalState.handleStop();
|
await globalState.handleStop();
|
||||||
await clashCore.resetTraffic();
|
await clashCore.resetTraffic();
|
||||||
appFlowingState.traffics = [];
|
_ref.read(trafficsProvider.notifier).clear();
|
||||||
appFlowingState.totalTraffic = Traffic();
|
_ref.read(totalTrafficProvider.notifier).value = Traffic();
|
||||||
appFlowingState.runTime = null;
|
_ref.read(runTimeProvider.notifier).value = null;
|
||||||
|
// tray.updateTrayTitle(null);
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,100 +104,214 @@ class AppController {
|
|||||||
if (startTime != null) {
|
if (startTime != null) {
|
||||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
_ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
|
||||||
} else {
|
} else {
|
||||||
appFlowingState.runTime = null;
|
_ref.read(runTimeProvider.notifier).value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTraffic() {
|
updateTraffic() async {
|
||||||
globalState.updateTraffic(
|
final traffic = await clashCore.getTraffic();
|
||||||
config: config,
|
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
|
||||||
appFlowingState: appFlowingState,
|
_ref.read(totalTrafficProvider.notifier).value =
|
||||||
);
|
await clashCore.getTotalTraffic();
|
||||||
}
|
}
|
||||||
|
|
||||||
addProfile(Profile profile) async {
|
addProfile(Profile profile) async {
|
||||||
config.setProfile(profile);
|
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||||
if (config.currentProfileId != null) return;
|
if (_ref.read(currentProfileIdProvider) != null) return;
|
||||||
await changeProfile(profile.id);
|
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteProfile(String id) async {
|
deleteProfile(String id) async {
|
||||||
config.deleteProfileById(id);
|
_ref.read(profilesProvider.notifier).deleteProfileById(id);
|
||||||
clearEffect(id);
|
clearEffect(id);
|
||||||
if (config.currentProfileId == id) {
|
if (globalState.config.currentProfileId == id) {
|
||||||
if (config.profiles.isNotEmpty) {
|
final profiles = globalState.config.profiles;
|
||||||
final updateId = config.profiles.first.id;
|
final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
|
||||||
changeProfile(updateId);
|
if (profiles.isNotEmpty) {
|
||||||
|
final updateId = profiles.first.id;
|
||||||
|
currentProfileId.value = updateId;
|
||||||
} else {
|
} else {
|
||||||
changeProfile(null);
|
currentProfileId.value = null;
|
||||||
updateStatus(false);
|
updateStatus(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProviders() async {
|
updateProviders() async {
|
||||||
await globalState.updateProviders(appState);
|
_ref.read(providersProvider.notifier).value =
|
||||||
|
await clashCore.getExternalProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalIp() async {
|
updateLocalIp() async {
|
||||||
appFlowingState.localIp = null;
|
_ref.read(localIpProvider.notifier).value = null;
|
||||||
await Future.delayed(commonDuration);
|
await Future.delayed(commonDuration);
|
||||||
appFlowingState.localIp = await other.getLocalIpAddress();
|
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateProfile(Profile profile) async {
|
Future<void> updateProfile(Profile profile) async {
|
||||||
final newProfile = await profile.update();
|
final newProfile = await profile.update();
|
||||||
config.setProfile(
|
_ref
|
||||||
newProfile.copyWith(isUpdating: false),
|
.read(profilesProvider.notifier)
|
||||||
);
|
.setProfile(newProfile.copyWith(isUpdating: false));
|
||||||
if (profile.id == config.currentProfile?.id) {
|
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||||
applyProfileDebounce();
|
applyProfileDebounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
_setProfile(Profile profile) {
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfile(Profile profile) {
|
||||||
|
_setProfile(profile);
|
||||||
|
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||||
|
applyProfileDebounce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfiles(List<Profile> profiles) {
|
||||||
|
_ref.read(profilesProvider.notifier).value = profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog(Log log) {
|
||||||
|
_ref.read(logsProvider).add(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
|
||||||
|
final hotKeyActions = _ref.read(hotKeyActionsProvider);
|
||||||
|
final index =
|
||||||
|
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
|
||||||
|
if (index == -1) {
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
|
||||||
|
..add(hotKeyAction);
|
||||||
|
} else {
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
|
||||||
|
..[index] = hotKeyAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
|
||||||
|
? (List.from(hotKeyActions)..add(hotKeyAction))
|
||||||
|
: (List.from(hotKeyActions)..[index] = hotKeyAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Group> getCurrentGroups() {
|
||||||
|
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
String getRealTestUrl(String? url) {
|
||||||
|
return _ref.read(getRealTestUrlProvider(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
int getProxiesColumns() {
|
||||||
|
return _ref.read(getProxiesColumnsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSortNum() {
|
||||||
|
return _ref.read(sortNumProvider.notifier).add();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentGroupName() {
|
||||||
|
final currentGroupName = _ref.read(currentProfileProvider.select(
|
||||||
|
(state) => state?.currentGroupName,
|
||||||
|
));
|
||||||
|
return currentGroupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRealProxyName(proxyName) {
|
||||||
|
return _ref.read(getRealTestUrlProvider(proxyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedProxyName(groupName) {
|
||||||
|
return _ref.read(getSelectedProxyNameProvider(groupName));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentGroupName(String groupName) {
|
||||||
|
final profile = _ref.read(currentProfileProvider);
|
||||||
|
if (profile == null || profile.currentGroupName == groupName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_setProfile(
|
||||||
|
profile.copyWith(currentGroupName: groupName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateClashConfig([bool? isPatch]) async {
|
||||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||||
if (commonScaffoldState?.mounted != true) return;
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
await commonScaffoldState?.loadingRun(() async {
|
await commonScaffoldState?.loadingRun(() async {
|
||||||
await globalState.updateClashConfig(
|
await _updateClashConfig(
|
||||||
appState: appState,
|
isPatch,
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
|
||||||
isPatch: isPatch,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future applyProfile({bool isPrue = false}) async {
|
Future<void> _updateClashConfig([bool? isPatch]) async {
|
||||||
if (isPrue) {
|
final profile = _ref.watch(currentProfileProvider);
|
||||||
await globalState.applyProfile(
|
await _ref.read(currentProfileProvider)?.checkAndUpdate();
|
||||||
appState: appState,
|
final patchConfig = _ref.read(patchClashConfigProvider);
|
||||||
config: config,
|
final appSetting = _ref.read(appSettingProvider);
|
||||||
clashConfig: clashConfig,
|
bool enableTun = patchConfig.tun.enable;
|
||||||
);
|
if (enableTun != lastTunEnable &&
|
||||||
|
lastTunEnable == false &&
|
||||||
|
!Platform.isAndroid) {
|
||||||
|
final code = await system.authorizeCore();
|
||||||
|
switch (code) {
|
||||||
|
case AuthorizeCode.none:
|
||||||
|
break;
|
||||||
|
case AuthorizeCode.success:
|
||||||
|
lastTunEnable = enableTun;
|
||||||
|
await restartCore();
|
||||||
|
return;
|
||||||
|
case AuthorizeCode.error:
|
||||||
|
enableTun = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (appSetting.openLogs) {
|
||||||
|
clashCore.startLog();
|
||||||
|
} else {
|
||||||
|
clashCore.stopLog();
|
||||||
|
}
|
||||||
|
final res = await clashCore.updateConfig(
|
||||||
|
globalState.getUpdateConfigParams(isPatch),
|
||||||
|
);
|
||||||
|
if (res.isNotEmpty) throw res;
|
||||||
|
lastTunEnable = enableTun;
|
||||||
|
lastProfileModified = await profile?.profileLastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _applyProfile() async {
|
||||||
|
await clashCore.requestGc();
|
||||||
|
await updateClashConfig();
|
||||||
|
await updateGroups();
|
||||||
|
await updateProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future applyProfile({bool silence = false}) async {
|
||||||
|
if (silence) {
|
||||||
|
await _applyProfile();
|
||||||
} else {
|
} else {
|
||||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||||
if (commonScaffoldState?.mounted != true) return;
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
await commonScaffoldState?.loadingRun(() async {
|
await commonScaffoldState?.loadingRun(() async {
|
||||||
await globalState.applyProfile(
|
await _applyProfile();
|
||||||
appState: appState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
changeProfile(String? value) async {
|
handleChangeProfile() {
|
||||||
if (value == config.currentProfileId) return;
|
_ref.read(delayDataSourceProvider.notifier).value = {};
|
||||||
config.currentProfileId = value;
|
applyProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBrightness(Brightness brightness) {
|
||||||
|
_ref.read(appBrightnessProvider.notifier).value = brightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
autoUpdateProfiles() async {
|
autoUpdateProfiles() async {
|
||||||
for (final profile in config.profiles) {
|
for (final profile in _ref.read(profilesProvider)) {
|
||||||
if (!profile.autoUpdate) continue;
|
if (!profile.autoUpdate) continue;
|
||||||
final isNotNeedUpdate = profile.lastUpdateDate
|
final isNotNeedUpdate = profile.lastUpdateDate
|
||||||
?.add(
|
?.add(
|
||||||
@@ -214,20 +322,29 @@ class AppController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updateProfile(profile);
|
await updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appFlowingState.addLog(
|
_ref.read(logsProvider.notifier).addLog(
|
||||||
Log(
|
Log(
|
||||||
logLevel: LogLevel.info,
|
logLevel: LogLevel.info,
|
||||||
payload: e.toString(),
|
payload: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateGroups() async {
|
||||||
|
_ref.read(groupsProvider.notifier).value = await retry(
|
||||||
|
task: () async {
|
||||||
|
return await clashCore.getProxiesGroups();
|
||||||
|
},
|
||||||
|
retryIf: (res) => res.isEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateProfiles() async {
|
updateProfiles() async {
|
||||||
for (final profile in config.profiles) {
|
for (final profile in _ref.read(profilesProvider)) {
|
||||||
if (profile.type == ProfileType.file) {
|
if (profile.type == ProfileType.file) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -235,34 +352,33 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateGroups() async {
|
updateSystemColorSchemes(ColorSchemes colorSchemes) {
|
||||||
await globalState.updateGroups(appState);
|
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
|
||||||
}
|
|
||||||
|
|
||||||
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
|
|
||||||
appState.systemColorSchemes = systemColorSchemes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
savePreferences() async {
|
savePreferences() async {
|
||||||
debugPrint("[APP] savePreferences");
|
commonPrint.log("save preferences");
|
||||||
await preferences.saveConfig(config);
|
await preferences.saveConfig(globalState.config);
|
||||||
await preferences.saveClashConfig(clashConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeProxy({
|
changeProxy({
|
||||||
required String groupName,
|
required String groupName,
|
||||||
required String proxyName,
|
required String proxyName,
|
||||||
}) async {
|
}) async {
|
||||||
await globalState.changeProxy(
|
await clashCore.changeProxy(
|
||||||
config: config,
|
ChangeProxyParams(
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
proxyName: proxyName,
|
proxyName: proxyName,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (_ref.read(appSettingProvider).closeConnections) {
|
||||||
|
clashCore.closeConnections();
|
||||||
|
}
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBackOrExit() async {
|
handleBackOrExit() async {
|
||||||
if (config.appSetting.minimizeOnExit) {
|
if (_ref.read(appSettingProvider).minimizeOnExit) {
|
||||||
if (system.isDesktop) {
|
if (system.isDesktop) {
|
||||||
await savePreferencesDebounce();
|
await savePreferencesDebounce();
|
||||||
}
|
}
|
||||||
@@ -279,12 +395,13 @@ class AppController {
|
|||||||
await clashService?.destroy();
|
await clashService?.destroy();
|
||||||
await proxy?.stopProxy();
|
await proxy?.stopProxy();
|
||||||
await savePreferences();
|
await savePreferences();
|
||||||
} catch (_) {}
|
} finally {
|
||||||
system.exit();
|
system.exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
autoCheckUpdate() async {
|
autoCheckUpdate() async {
|
||||||
if (!config.appSetting.autoCheckUpdate) return;
|
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
|
||||||
final res = await request.checkForUpdate();
|
final res = await request.checkForUpdate();
|
||||||
checkUpdateResultHandle(data: res);
|
checkUpdateResultHandle(data: res);
|
||||||
}
|
}
|
||||||
@@ -298,7 +415,7 @@ class AppController {
|
|||||||
final body = data['body'];
|
final body = data['body'];
|
||||||
final submits = other.parseReleaseBody(body);
|
final submits = other.parseReleaseBody(body);
|
||||||
final textTheme = context.textTheme;
|
final textTheme = context.textTheme;
|
||||||
globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
title: appLocalizations.discoverNewVersion,
|
title: appLocalizations.discoverNewVersion,
|
||||||
message: TextSpan(
|
message: TextSpan(
|
||||||
text: "$tagName \n",
|
text: "$tagName \n",
|
||||||
@@ -315,13 +432,14 @@ class AppController {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTab: () {
|
|
||||||
launchUrl(
|
|
||||||
Uri.parse("https://github.com/$repository/releases/latest"),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
confirmText: appLocalizations.goDownload,
|
confirmText: appLocalizations.goDownload,
|
||||||
);
|
);
|
||||||
|
if (res != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://github.com/$repository/releases/latest"),
|
||||||
|
);
|
||||||
} else if (handleError) {
|
} else if (handleError) {
|
||||||
globalState.showMessage(
|
globalState.showMessage(
|
||||||
title: appLocalizations.checkUpdate,
|
title: appLocalizations.checkUpdate,
|
||||||
@@ -332,33 +450,61 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() async {
|
_handlePreference() async {
|
||||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
if (await preferences.isInit) {
|
||||||
if (!isDisclaimerAccepted) {
|
return;
|
||||||
handleExit();
|
|
||||||
}
|
}
|
||||||
if (!config.appSetting.silentLaunch) {
|
final res = await globalState.showMessage(
|
||||||
window?.show();
|
title: appLocalizations.tip,
|
||||||
}
|
message: TextSpan(text: appLocalizations.cacheCorrupt),
|
||||||
await globalState.initCore(
|
|
||||||
appState: appState,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
|
||||||
);
|
);
|
||||||
|
if (res == true) {
|
||||||
|
final file = File(await appPath.sharedPreferencesPath);
|
||||||
|
final isExists = await file.exists();
|
||||||
|
if (isExists) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await handleExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initCore() async {
|
||||||
|
final isInit = await clashCore.isInit;
|
||||||
|
if (!isInit) {
|
||||||
|
await clashCore.setState(
|
||||||
|
globalState.getCoreState(),
|
||||||
|
);
|
||||||
|
await clashCore.init();
|
||||||
|
}
|
||||||
|
await applyProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() async {
|
||||||
|
await _handlePreference();
|
||||||
|
await _handlerDisclaimer();
|
||||||
|
await initCore();
|
||||||
await _initStatus();
|
await _initStatus();
|
||||||
|
updateTray(true);
|
||||||
autoLaunch?.updateStatus(
|
autoLaunch?.updateStatus(
|
||||||
config.appSetting.autoLaunch,
|
_ref.read(appSettingProvider).autoLaunch,
|
||||||
);
|
);
|
||||||
autoUpdateProfiles();
|
autoUpdateProfiles();
|
||||||
autoCheckUpdate();
|
autoCheckUpdate();
|
||||||
|
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||||
|
window?.show();
|
||||||
|
} else {
|
||||||
|
window?.hide();
|
||||||
|
}
|
||||||
|
_ref.read(initProvider.notifier).value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initStatus() async {
|
_initStatus() async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
globalState.updateStartTime();
|
await globalState.updateStartTime();
|
||||||
}
|
}
|
||||||
final status =
|
final status = globalState.isStart == true
|
||||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
? true
|
||||||
|
: _ref.read(appSettingProvider).autoRun;
|
||||||
|
|
||||||
await updateStatus(status);
|
await updateStatus(status);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -367,15 +513,23 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDelay(Delay delay) {
|
setDelay(Delay delay) {
|
||||||
appState.setDelay(delay);
|
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
toPage(int index, {bool hasAnimate = false}) {
|
toPage(
|
||||||
if (index > appState.currentNavigationItems.length - 1) {
|
int index, {
|
||||||
|
bool hasAnimate = false,
|
||||||
|
}) {
|
||||||
|
final navigations = _ref.read(currentNavigationsStateProvider).value;
|
||||||
|
if (index > navigations.length - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appState.currentLabel = appState.currentNavigationItems[index].label;
|
_ref.read(currentPageLabelProvider.notifier).value =
|
||||||
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
|
navigations[index].label;
|
||||||
|
final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage;
|
||||||
|
final isMobile =
|
||||||
|
_ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile;
|
||||||
|
if (isAnimateToPage && isMobile || hasAnimate) {
|
||||||
globalState.pageController?.animateToPage(
|
globalState.pageController?.animateToPage(
|
||||||
index,
|
index,
|
||||||
duration: kTabScrollDuration,
|
duration: kTabScrollDuration,
|
||||||
@@ -387,9 +541,9 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toProfiles() {
|
toProfiles() {
|
||||||
final index = appState.currentNavigationItems.indexWhere(
|
final index = _ref.read(currentNavigationsStateProvider).value.indexWhere(
|
||||||
(element) => element.label == "profiles",
|
(element) => element.label == PageLabel.profiles,
|
||||||
);
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
toPage(index);
|
toPage(index);
|
||||||
}
|
}
|
||||||
@@ -397,8 +551,8 @@ class AppController {
|
|||||||
|
|
||||||
initLink() {
|
initLink() {
|
||||||
linkManager.initAppLinksListen(
|
linkManager.initAppLinksListen(
|
||||||
(url) {
|
(url) async {
|
||||||
globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
title: "${appLocalizations.add}${appLocalizations.profile}",
|
title: "${appLocalizations.add}${appLocalizations.profile}",
|
||||||
message: TextSpan(
|
message: TextSpan(
|
||||||
children: [
|
children: [
|
||||||
@@ -416,10 +570,12 @@ class AppController {
|
|||||||
"${appLocalizations.create}${appLocalizations.profile}"),
|
"${appLocalizations.create}${appLocalizations.profile}"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTab: () {
|
|
||||||
addProfileFormURL(url);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (res != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addProfileFormURL(url);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -447,9 +603,9 @@ class AppController {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
config.appSetting = config.appSetting.copyWith(
|
_ref.read(appSettingProvider.notifier).updateState(
|
||||||
disclaimerAccepted: true,
|
(state) => state.copyWith(disclaimerAccepted: true),
|
||||||
);
|
);
|
||||||
Navigator.of(context).pop<bool>(true);
|
Navigator.of(context).pop<bool>(true);
|
||||||
},
|
},
|
||||||
child: Text(appLocalizations.agree),
|
child: Text(appLocalizations.agree),
|
||||||
@@ -460,11 +616,15 @@ class AppController {
|
|||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> handlerDisclaimer() async {
|
_handlerDisclaimer() async {
|
||||||
if (config.appSetting.disclaimerAccepted) {
|
if (_ref.read(appSettingProvider).disclaimerAccepted) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
return showDisclaimer();
|
final isDisclaimerAccepted = await showDisclaimer();
|
||||||
|
if (!isDisclaimerAccepted) {
|
||||||
|
await handleExit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addProfileFormURL(String url) async {
|
addProfileFormURL(String url) async {
|
||||||
@@ -518,10 +678,14 @@ class AppController {
|
|||||||
|
|
||||||
updateViewWidth(double width) {
|
updateViewWidth(double width) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
appState.viewWidth = width;
|
_ref.read(viewWidthProvider.notifier).value = width;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProvider(ExternalProvider? provider) {
|
||||||
|
_ref.read(providersProvider.notifier).setProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||||
return List.of(proxies)
|
return List.of(proxies)
|
||||||
..sort(
|
..sort(
|
||||||
@@ -532,12 +696,17 @@ class AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
|
List<Proxy> _sortOfDelay({
|
||||||
return proxies = List.of(proxies)
|
required List<Proxy> proxies,
|
||||||
|
String? testUrl,
|
||||||
|
}) {
|
||||||
|
return List.of(proxies)
|
||||||
..sort(
|
..sort(
|
||||||
(a, b) {
|
(a, b) {
|
||||||
final aDelay = appState.getDelay(a.name);
|
final aDelay =
|
||||||
final bDelay = appState.getDelay(b.name);
|
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
|
||||||
|
final bDelay =
|
||||||
|
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
|
||||||
if (aDelay == null && bDelay == null) {
|
if (aDelay == null && bDelay == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -552,21 +721,17 @@ class AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||||
return switch (config.proxiesStyle.sortType) {
|
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
|
||||||
ProxiesSortType.none => proxies,
|
ProxiesSortType.none => proxies,
|
||||||
ProxiesSortType.delay => _sortOfDelay(proxies),
|
ProxiesSortType.delay => _sortOfDelay(
|
||||||
|
proxies: proxies,
|
||||||
|
testUrl: url,
|
||||||
|
),
|
||||||
ProxiesSortType.name => _sortOfName(proxies),
|
ProxiesSortType.name => _sortOfName(proxies),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
String getCurrentSelectedName(String groupName) {
|
|
||||||
final group = appState.getGroupWithName(groupName);
|
|
||||||
return group?.getCurrentSelectedName(
|
|
||||||
config.currentSelectedMap[groupName] ?? '') ??
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
|
|
||||||
clearEffect(String profileId) async {
|
clearEffect(String profileId) async {
|
||||||
final profilePath = await appPath.getProfilePath(profileId);
|
final profilePath = await appPath.getProfilePath(profileId);
|
||||||
final providersPath = await appPath.getProvidersPath(profileId);
|
final providersPath = await appPath.getProvidersPath(profileId);
|
||||||
@@ -581,33 +746,66 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTun() {
|
updateTun() {
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
enable: !clashConfig.tun.enable,
|
(state) => state.copyWith.tun(enable: !state.tun.enable),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSystemProxy() {
|
updateSystemProxy() {
|
||||||
config.networkProps = config.networkProps.copyWith(
|
_ref.read(networkSettingProvider.notifier).updateState(
|
||||||
systemProxy: !config.networkProps.systemProxy,
|
(state) => state.copyWith(
|
||||||
);
|
systemProxy: state.systemProxy,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStart() {
|
updateStart() {
|
||||||
updateStatus(!appFlowingState.isStart);
|
updateStatus(_ref.read(runTimeProvider.notifier).isStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentSelectedMap(String groupName, String proxyName) {
|
||||||
|
final currentProfile = _ref.read(currentProfileProvider);
|
||||||
|
if (currentProfile != null &&
|
||||||
|
currentProfile.selectedMap[groupName] != proxyName) {
|
||||||
|
final SelectedMap selectedMap = Map.from(
|
||||||
|
currentProfile.selectedMap,
|
||||||
|
)..[groupName] = proxyName;
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(
|
||||||
|
currentProfile.copyWith(
|
||||||
|
selectedMap: selectedMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentUnfoldSet(Set<String> value) {
|
||||||
|
final currentProfile = _ref.read(currentProfileProvider);
|
||||||
|
if (currentProfile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(
|
||||||
|
currentProfile.copyWith(
|
||||||
|
unfoldSet: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMode(Mode mode) {
|
changeMode(Mode mode) {
|
||||||
clashConfig.mode = mode;
|
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(mode: mode),
|
||||||
|
);
|
||||||
if (mode == Mode.global) {
|
if (mode == Mode.global) {
|
||||||
config.updateCurrentGroupName(GroupName.GLOBAL.name);
|
updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||||
}
|
}
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAutoLaunch() {
|
updateAutoLaunch() {
|
||||||
config.appSetting = config.appSetting.copyWith(
|
_ref.read(appSettingProvider.notifier).updateState(
|
||||||
autoLaunch: !config.appSetting.autoLaunch,
|
(state) => state.copyWith(
|
||||||
);
|
autoLaunch: !state.autoLaunch,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVisible() async {
|
updateVisible() async {
|
||||||
@@ -620,18 +818,24 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateMode() {
|
updateMode() {
|
||||||
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
|
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
if (index == -1) {
|
(state) {
|
||||||
return;
|
final index = Mode.values.indexWhere((item) => item == state.mode);
|
||||||
}
|
if (index == -1) {
|
||||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
return null;
|
||||||
clashConfig.mode = Mode.values[nextIndex];
|
}
|
||||||
|
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||||
|
return state.copyWith(
|
||||||
|
mode: Mode.values[nextIndex],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> exportLogs() async {
|
Future<bool> exportLogs() async {
|
||||||
final logsRaw = appFlowingState.logs.map(
|
final logsRaw = _ref.read(logsProvider).list.map(
|
||||||
(item) => item.toString(),
|
(item) => item.toString(),
|
||||||
);
|
);
|
||||||
final data = await Isolate.run<List<int>>(() async {
|
final data = await Isolate.run<List<int>>(() async {
|
||||||
final logsRawString = logsRaw.join("\n");
|
final logsRawString = logsRaw.join("\n");
|
||||||
return utf8.encode(logsRawString);
|
return utf8.encode(logsRawString);
|
||||||
@@ -644,14 +848,12 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<int>> backupData() async {
|
Future<List<int>> backupData() async {
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
final profilesPath = await appPath.getProfilesPath();
|
final profilesPath = await appPath.profilesPath;
|
||||||
final configJson = config.toJson();
|
final configJson = globalState.config.toJson();
|
||||||
final clashConfigJson = clashConfig.toJson();
|
|
||||||
return Isolate.run<List<int>>(() async {
|
return Isolate.run<List<int>>(() async {
|
||||||
final archive = Archive();
|
final archive = Archive();
|
||||||
archive.add("config.json", configJson);
|
archive.add("config.json", configJson);
|
||||||
archive.add("clashConfig.json", clashConfigJson);
|
|
||||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||||
final zipEncoder = ZipEncoder();
|
final zipEncoder = ZipEncoder();
|
||||||
return zipEncoder.encode(archive) ?? [];
|
return zipEncoder.encode(archive) ?? [];
|
||||||
@@ -660,11 +862,7 @@ class AppController {
|
|||||||
|
|
||||||
updateTray([bool focus = false]) async {
|
updateTray([bool focus = false]) async {
|
||||||
tray.update(
|
tray.update(
|
||||||
appState: appState,
|
trayState: _ref.read(trayStateProvider),
|
||||||
appFlowingState: appFlowingState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
focus: focus,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,39 +874,71 @@ class AppController {
|
|||||||
final zipDecoder = ZipDecoder();
|
final zipDecoder = ZipDecoder();
|
||||||
return zipDecoder.decodeBytes(data);
|
return zipDecoder.decodeBytes(data);
|
||||||
});
|
});
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
final configs =
|
final configs =
|
||||||
archive.files.where((item) => item.name.endsWith(".json")).toList();
|
archive.files.where((item) => item.name.endsWith(".json")).toList();
|
||||||
final profiles =
|
final profiles =
|
||||||
archive.files.where((item) => !item.name.endsWith(".json"));
|
archive.files.where((item) => !item.name.endsWith(".json"));
|
||||||
final configIndex =
|
final configIndex =
|
||||||
configs.indexWhere((config) => config.name == "config.json");
|
configs.indexWhere((config) => config.name == "config.json");
|
||||||
final clashConfigIndex =
|
if (configIndex == -1) throw "invalid backup file";
|
||||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
|
||||||
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
|
|
||||||
final configFile = configs[configIndex];
|
final configFile = configs[configIndex];
|
||||||
final clashConfigFile = configs[clashConfigIndex];
|
var tempConfig = Config.compatibleFromJson(
|
||||||
final tempConfig = Config.fromJson(
|
|
||||||
json.decode(
|
json.decode(
|
||||||
utf8.decode(configFile.content),
|
utf8.decode(configFile.content),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final tempClashConfig = ClashConfig.fromJson(
|
|
||||||
json.decode(
|
|
||||||
utf8.decode(clashConfigFile.content),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for (final profile in profiles) {
|
for (final profile in profiles) {
|
||||||
final filePath = join(homeDirPath, profile.name);
|
final filePath = join(homeDirPath, profile.name);
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
await file.create(recursive: true);
|
await file.create(recursive: true);
|
||||||
await file.writeAsBytes(profile.content);
|
await file.writeAsBytes(profile.content);
|
||||||
}
|
}
|
||||||
if (recoveryOption == RecoveryOption.onlyProfiles) {
|
final clashConfigIndex =
|
||||||
config.update(tempConfig, RecoveryOption.onlyProfiles);
|
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||||
} else {
|
if (clashConfigIndex != -1) {
|
||||||
config.update(tempConfig, RecoveryOption.all);
|
final clashConfigFile = configs[clashConfigIndex];
|
||||||
clashConfig.update(tempClashConfig);
|
tempConfig = tempConfig.copyWith(
|
||||||
|
patchClashConfig: ClashConfig.fromJson(
|
||||||
|
json.decode(
|
||||||
|
utf8.decode(
|
||||||
|
clashConfigFile.content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
_recovery(
|
||||||
|
tempConfig,
|
||||||
|
recoveryOption,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_recovery(Config config, RecoveryOption recoveryOption) {
|
||||||
|
final profiles = config.profiles;
|
||||||
|
for (final profile in profiles) {
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||||
|
}
|
||||||
|
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
|
||||||
|
if (onlyProfiles) {
|
||||||
|
final currentProfile = _ref.read(currentProfileProvider);
|
||||||
|
if (currentProfile != null) {
|
||||||
|
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ref.read(patchClashConfigProvider.notifier).value =
|
||||||
|
config.patchClashConfig;
|
||||||
|
_ref.read(appSettingProvider.notifier).value = config.appSetting;
|
||||||
|
_ref.read(currentProfileIdProvider.notifier).value =
|
||||||
|
config.currentProfileId;
|
||||||
|
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
|
||||||
|
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
|
||||||
|
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
|
||||||
|
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
|
||||||
|
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
|
||||||
|
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
|
||||||
|
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,24 @@ const desktopPlatforms = [
|
|||||||
SupportPlatform.Windows,
|
SupportPlatform.Windows,
|
||||||
];
|
];
|
||||||
|
|
||||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
enum GroupType {
|
||||||
|
Selector,
|
||||||
|
URLTest,
|
||||||
|
Fallback,
|
||||||
|
LoadBalance,
|
||||||
|
Relay;
|
||||||
|
|
||||||
|
static GroupType parseProfileType(String type) {
|
||||||
|
return switch (type) {
|
||||||
|
"url-test" => URLTest,
|
||||||
|
"select" => Selector,
|
||||||
|
"fallback" => Fallback,
|
||||||
|
"load-balance" => LoadBalance,
|
||||||
|
"relay" => Relay,
|
||||||
|
String() => throw UnimplementedError(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||||
|
|
||||||
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
bool get isURLTestOrFallback {
|
bool get isComputedSelected {
|
||||||
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +117,12 @@ enum AppMessageType {
|
|||||||
log,
|
log,
|
||||||
delay,
|
delay,
|
||||||
request,
|
request,
|
||||||
started,
|
|
||||||
loaded,
|
loaded,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ServiceMessageType {
|
enum InvokeMessageType {
|
||||||
protect,
|
protect,
|
||||||
process,
|
process,
|
||||||
started,
|
|
||||||
loaded,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FindProcessMode { always, off }
|
enum FindProcessMode { always, off }
|
||||||
@@ -141,6 +155,13 @@ enum DnsMode {
|
|||||||
hosts
|
hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ExternalControllerStatus {
|
||||||
|
@JsonValue("")
|
||||||
|
close,
|
||||||
|
@JsonValue("127.0.0.1:9090")
|
||||||
|
open
|
||||||
|
}
|
||||||
|
|
||||||
enum KeyboardModifier {
|
enum KeyboardModifier {
|
||||||
alt([
|
alt([
|
||||||
PhysicalKeyboardKey.altLeft,
|
PhysicalKeyboardKey.altLeft,
|
||||||
@@ -241,6 +262,18 @@ enum ActionMethod {
|
|||||||
stopListener,
|
stopListener,
|
||||||
getCountryCode,
|
getCountryCode,
|
||||||
getMemory,
|
getMemory,
|
||||||
|
getProfile,
|
||||||
|
|
||||||
|
///Android,
|
||||||
|
setFdMap,
|
||||||
|
setProcessMap,
|
||||||
|
setState,
|
||||||
|
startTun,
|
||||||
|
stopTun,
|
||||||
|
getRunTime,
|
||||||
|
updateDns,
|
||||||
|
getAndroidVpnOptions,
|
||||||
|
getCurrentProfileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuthorizeCode { none, success, error }
|
enum AuthorizeCode { none, success, error }
|
||||||
@@ -262,7 +295,11 @@ enum DebounceTag {
|
|||||||
handleWill,
|
handleWill,
|
||||||
updateDelay,
|
updateDelay,
|
||||||
vpnTip,
|
vpnTip,
|
||||||
autoLaunch
|
autoLaunch,
|
||||||
|
renderPause,
|
||||||
|
updatePageIndex,
|
||||||
|
pageChange,
|
||||||
|
proxiesTabChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DashboardWidget {
|
enum DashboardWidget {
|
||||||
@@ -333,3 +370,19 @@ enum DashboardWidget {
|
|||||||
return dashboardWidgets[index];
|
return dashboardWidgets[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum GeodataLoader {
|
||||||
|
standard,
|
||||||
|
memconservative,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PageLabel {
|
||||||
|
dashboard,
|
||||||
|
proxies,
|
||||||
|
profiles,
|
||||||
|
tools,
|
||||||
|
logs,
|
||||||
|
requests,
|
||||||
|
resources,
|
||||||
|
connections,
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,41 +4,50 @@ import 'package:fl_clash/enum/enum.dart';
|
|||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class AccessFragment extends StatefulWidget {
|
class AccessFragment extends ConsumerStatefulWidget {
|
||||||
const AccessFragment({super.key});
|
const AccessFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccessFragment> createState() => _AccessFragmentState();
|
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccessFragmentState extends State<AccessFragment> {
|
class _AccessFragmentState extends ConsumerState<AccessFragment> {
|
||||||
List<String> acceptList = [];
|
List<String> acceptList = [];
|
||||||
List<String> rejectList = [];
|
List<String> rejectList = [];
|
||||||
|
late ScrollController _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_updateInitList();
|
_updateInitList();
|
||||||
|
_controller = ScrollController();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final appState = globalState.appController.appState;
|
final appState = globalState.appState;
|
||||||
if (appState.packages.isEmpty) {
|
if (appState.packages.isEmpty) {
|
||||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||||
appState.packages = await app?.getPackages() ?? [];
|
ref.read(packagesProvider.notifier).value =
|
||||||
|
await app?.getPackages() ?? [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
_updateInitList() {
|
_updateInitList() {
|
||||||
final accessControl = globalState.appController.config.accessControl;
|
acceptList = globalState.config.vpnProps.accessControl.acceptList;
|
||||||
acceptList = accessControl.acceptList;
|
rejectList = globalState.config.vpnProps.accessControl.rejectList;
|
||||||
rejectList = accessControl.rejectList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSearchButton() {
|
Widget _buildSearchButton() {
|
||||||
@@ -51,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
acceptList: acceptList,
|
acceptList: acceptList,
|
||||||
rejectList: rejectList,
|
rejectList: rejectList,
|
||||||
),
|
),
|
||||||
).then((_) => setState(() {
|
).then(
|
||||||
_updateInitList();
|
(_) => setState(
|
||||||
}));
|
() {
|
||||||
|
_updateInitList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
);
|
);
|
||||||
@@ -69,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||||
final isAccept =
|
final isAccept =
|
||||||
config.accessControl.mode == AccessControlMode.acceptSelected;
|
state.accessControl.mode == AccessControlMode.acceptSelected;
|
||||||
if (isSelectedAll) {
|
if (isSelectedAll) {
|
||||||
config.accessControl = switch (isAccept) {
|
return switch (isAccept) {
|
||||||
true => config.accessControl.copyWith(
|
true => state.copyWith.accessControl(
|
||||||
acceptList: [],
|
acceptList: [],
|
||||||
),
|
),
|
||||||
false => config.accessControl.copyWith(
|
false => state.copyWith.accessControl(
|
||||||
rejectList: [],
|
rejectList: [],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
config.accessControl = switch (isAccept) {
|
return switch (isAccept) {
|
||||||
true => config.accessControl.copyWith(
|
true => state.copyWith.accessControl(
|
||||||
acceptList: allValueList,
|
acceptList: allValueList,
|
||||||
),
|
),
|
||||||
false => config.accessControl.copyWith(
|
false => state.copyWith.accessControl(
|
||||||
rejectList: allValueList,
|
rejectList: allValueList,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: isSelectedAll
|
icon: isSelectedAll
|
||||||
? const Icon(Icons.deselect)
|
? const Icon(Icons.deselect)
|
||||||
@@ -98,218 +112,239 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSettingButton() {
|
_intelligentSelected() async {
|
||||||
return IconButton(
|
final appState = globalState.appState;
|
||||||
onPressed: () {
|
final config = globalState.config;
|
||||||
showSheet(
|
final accessControl = config.vpnProps.accessControl;
|
||||||
title: appLocalizations.proxiesSetting,
|
final packageNames = appState.packages
|
||||||
context: context,
|
.where(
|
||||||
body: AccessControlWidget(
|
(item) =>
|
||||||
context: context,
|
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
||||||
|
)
|
||||||
|
.map((item) => item.packageName);
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
|
final selectedPackageNames =
|
||||||
|
(await commonScaffoldState?.loadingRun<List<String>>(
|
||||||
|
() async {
|
||||||
|
return await app?.getChinaPackageNames() ?? [];
|
||||||
|
},
|
||||||
|
))
|
||||||
|
?.toSet() ??
|
||||||
|
{};
|
||||||
|
final acceptList = packageNames
|
||||||
|
.where((item) => !selectedPackageNames.contains(item))
|
||||||
|
.toList();
|
||||||
|
final rejectList = packageNames
|
||||||
|
.where((item) => selectedPackageNames.contains(item))
|
||||||
|
.toList();
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith.accessControl(
|
||||||
|
acceptList: acceptList,
|
||||||
|
rejectList: rejectList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingButton() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final res = await showSheet<int>(
|
||||||
|
title: appLocalizations.proxiesSetting,
|
||||||
|
context: context,
|
||||||
|
body: AccessControlPanel(),
|
||||||
|
);
|
||||||
|
if (res == 1) {
|
||||||
|
_intelligentSelected();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.tune),
|
icon: const Icon(Icons.tune),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleSelected(List<String> valueList, Package package, bool? value) {
|
||||||
|
if (value == true) {
|
||||||
|
valueList.add(package.packageName);
|
||||||
|
} else {
|
||||||
|
valueList.remove(package.packageName);
|
||||||
|
}
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||||
|
return switch (
|
||||||
|
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
||||||
|
true => state.copyWith.accessControl(
|
||||||
|
acceptList: valueList,
|
||||||
|
),
|
||||||
|
false => state.copyWith.accessControl(
|
||||||
|
rejectList: valueList,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Config, bool>(
|
final state = ref.watch(packageListSelectorStateProvider);
|
||||||
selector: (_, config) => config.isAccessControl,
|
final accessControl = state.accessControl;
|
||||||
builder: (_, isAccessControl, child) {
|
final accessControlMode = accessControl.mode;
|
||||||
return Column(
|
final packages = state.getList(
|
||||||
mainAxisSize: MainAxisSize.max,
|
accessControlMode == AccessControlMode.acceptSelected
|
||||||
children: [
|
? acceptList
|
||||||
Flexible(
|
: rejectList,
|
||||||
flex: 0,
|
);
|
||||||
child: ListItem.switchItem(
|
final currentList = accessControl.currentList;
|
||||||
title: Text(appLocalizations.appAccessControl),
|
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||||
delegate: SwitchDelegate(
|
final valueList = currentList.intersection(packageNameList);
|
||||||
value: isAccessControl,
|
final describe = accessControlMode == AccessControlMode.acceptSelected
|
||||||
onChanged: (isAccessControl) {
|
? appLocalizations.accessControlAllowDesc
|
||||||
final config = context.read<Config>();
|
: appLocalizations.accessControlNotAllowDesc;
|
||||||
config.isAccessControl = isAccessControl;
|
return Column(
|
||||||
},
|
mainAxisSize: MainAxisSize.max,
|
||||||
),
|
children: [
|
||||||
),
|
Flexible(
|
||||||
|
flex: 0,
|
||||||
|
child: ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.appAccessControl),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: accessControl.enable,
|
||||||
|
onChanged: (enable) {
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith.accessControl(
|
||||||
|
enable: enable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Padding(
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
),
|
||||||
child: Divider(
|
const Padding(
|
||||||
height: 12,
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
child: Divider(
|
||||||
),
|
height: 12,
|
||||||
Flexible(
|
),
|
||||||
child: child!,
|
),
|
||||||
),
|
Flexible(
|
||||||
],
|
child: DisabledMask(
|
||||||
);
|
status: !accessControl.enable,
|
||||||
},
|
child: Column(
|
||||||
child: Selector<AppState, List<Package>>(
|
children: [
|
||||||
selector: (_, appState) => appState.packages,
|
ActivateBox(
|
||||||
builder: (_, packages, ___) {
|
active: accessControl.enable,
|
||||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
child: Padding(
|
||||||
selector: (_, appState, config) => PackageListSelectorState(
|
padding: const EdgeInsets.only(
|
||||||
accessControl: config.accessControl,
|
top: 4,
|
||||||
isAccessControl: config.isAccessControl,
|
bottom: 4,
|
||||||
packages: appState.packages,
|
left: 16,
|
||||||
),
|
right: 8,
|
||||||
builder: (context, state, __) {
|
),
|
||||||
final accessControl = state.accessControl;
|
child: Row(
|
||||||
final isAccessControl = state.isAccessControl;
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
final accessControlMode = accessControl.mode;
|
mainAxisSize: MainAxisSize.max,
|
||||||
final packages = state.getList(
|
children: [
|
||||||
accessControlMode == AccessControlMode.acceptSelected
|
Expanded(
|
||||||
? acceptList
|
child: IntrinsicHeight(
|
||||||
: rejectList,
|
child: Column(
|
||||||
);
|
mainAxisSize: MainAxisSize.max,
|
||||||
final currentList = accessControl.currentList;
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final packageNameList =
|
|
||||||
packages.map((e) => e.packageName).toList();
|
|
||||||
final valueList = currentList.intersection(packageNameList);
|
|
||||||
final describe =
|
|
||||||
accessControlMode == AccessControlMode.acceptSelected
|
|
||||||
? appLocalizations.accessControlAllowDesc
|
|
||||||
: appLocalizations.accessControlNotAllowDesc;
|
|
||||||
return DisabledMask(
|
|
||||||
status: !isAccessControl,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ActivateBox(
|
|
||||||
active: isAccessControl,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 4,
|
|
||||||
bottom: 4,
|
|
||||||
left: 16,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
appLocalizations.selected,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelLarge
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Flexible(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
"${valueList.length}",
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelLarge
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(describe),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Expanded(
|
||||||
child: _buildSearchButton(),
|
child: Row(
|
||||||
),
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _buildSelectedAllButton(
|
child: Text(
|
||||||
isSelectedAll: valueList.length ==
|
appLocalizations.selected,
|
||||||
packageNameList.length,
|
style: Theme.of(context)
|
||||||
allValueList: packageNameList,
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Flexible(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
"${valueList.length}",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _buildSettingButton(),
|
child: Text(describe),
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: _buildSearchButton(),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: _buildSelectedAllButton(
|
||||||
|
isSelectedAll:
|
||||||
|
valueList.length == packageNameList.length,
|
||||||
|
allValueList: packageNameList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: _buildSettingButton(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
flex: 1,
|
|
||||||
child: packages.isEmpty
|
|
||||||
? const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: packages.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final package = packages[index];
|
|
||||||
return PackageListItem(
|
|
||||||
key: Key(package.packageName),
|
|
||||||
package: package,
|
|
||||||
value:
|
|
||||||
valueList.contains(package.packageName),
|
|
||||||
isActive: isAccessControl,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == true) {
|
|
||||||
valueList.add(package.packageName);
|
|
||||||
} else {
|
|
||||||
valueList.remove(package.packageName);
|
|
||||||
}
|
|
||||||
final config =
|
|
||||||
globalState.appController.config;
|
|
||||||
if (accessControlMode ==
|
|
||||||
AccessControlMode.acceptSelected) {
|
|
||||||
config.accessControl =
|
|
||||||
config.accessControl.copyWith(
|
|
||||||
acceptList: valueList,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
config.accessControl =
|
|
||||||
config.accessControl.copyWith(
|
|
||||||
rejectList: valueList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
Expanded(
|
||||||
},
|
flex: 1,
|
||||||
);
|
child: packages.isEmpty
|
||||||
},
|
? const Center(
|
||||||
),
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: CommonScrollBar(
|
||||||
|
controller: _controller,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _controller,
|
||||||
|
itemCount: packages.length,
|
||||||
|
itemExtent: 72,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final package = packages[index];
|
||||||
|
return PackageListItem(
|
||||||
|
key: Key(package.packageName),
|
||||||
|
package: package,
|
||||||
|
value: valueList.contains(package.packageName),
|
||||||
|
isActive: accessControl.enable,
|
||||||
|
onChanged: (value) {
|
||||||
|
_handleSelected(valueList, package, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,45 +365,47 @@ class PackageListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActivateBox(
|
return FadeScaleEnterBox(
|
||||||
active: isActive,
|
child: ActivateBox(
|
||||||
child: ListItem.checkbox(
|
active: isActive,
|
||||||
leading: SizedBox(
|
child: ListItem.checkbox(
|
||||||
width: 48,
|
leading: SizedBox(
|
||||||
height: 48,
|
width: 48,
|
||||||
child: FutureBuilder<ImageProvider?>(
|
height: 48,
|
||||||
future: app?.getPackageIcon(package.packageName),
|
child: FutureBuilder<ImageProvider?>(
|
||||||
builder: (_, snapshot) {
|
future: app?.getPackageIcon(package.packageName),
|
||||||
if (!snapshot.hasData && snapshot.data == null) {
|
builder: (_, snapshot) {
|
||||||
return Container();
|
if (!snapshot.hasData && snapshot.data == null) {
|
||||||
} else {
|
return Container();
|
||||||
return Image(
|
} else {
|
||||||
image: snapshot.data!,
|
return Image(
|
||||||
gaplessPlayback: true,
|
image: snapshot.data!,
|
||||||
width: 48,
|
gaplessPlayback: true,
|
||||||
height: 48,
|
width: 48,
|
||||||
);
|
height: 48,
|
||||||
}
|
);
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
title: Text(
|
||||||
title: Text(
|
package.label,
|
||||||
package.label,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
subtitle: Text(
|
||||||
),
|
package.packageName,
|
||||||
subtitle: Text(
|
style: const TextStyle(
|
||||||
package.packageName,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
delegate: CheckboxDelegate(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
delegate: CheckboxDelegate(
|
|
||||||
value: value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -413,15 +450,31 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleSelected(
|
||||||
|
WidgetRef ref, List<String> valueList, Package package, bool? value) {
|
||||||
|
if (value == true) {
|
||||||
|
valueList.add(package.packageName);
|
||||||
|
} else {
|
||||||
|
valueList.remove(package.packageName);
|
||||||
|
}
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||||
|
return switch (
|
||||||
|
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
||||||
|
true => state.copyWith.accessControl(
|
||||||
|
acceptList: valueList,
|
||||||
|
),
|
||||||
|
false => state.copyWith.accessControl(
|
||||||
|
rejectList: valueList,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _packageList() {
|
Widget _packageList() {
|
||||||
final lowQuery = query.toLowerCase();
|
final lowQuery = query.toLowerCase();
|
||||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
return Consumer(
|
||||||
selector: (_, appState, config) => PackageListSelectorState(
|
builder: (context, ref, __) {
|
||||||
packages: appState.packages,
|
final state = ref.watch(packageListSelectorStateProvider);
|
||||||
accessControl: config.accessControl,
|
|
||||||
isAccessControl: config.isAccessControl,
|
|
||||||
),
|
|
||||||
builder: (context, state, __) {
|
|
||||||
final accessControl = state.accessControl;
|
final accessControl = state.accessControl;
|
||||||
final accessControlMode = accessControl.mode;
|
final accessControlMode = accessControl.mode;
|
||||||
final packages = state.getList(
|
final packages = state.getList(
|
||||||
@@ -436,7 +489,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
package.packageName.contains(lowQuery),
|
package.packageName.contains(lowQuery),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
final isAccessControl = state.isAccessControl;
|
final isAccessControl = state.accessControl.enable;
|
||||||
final currentList = accessControl.currentList;
|
final currentList = accessControl.currentList;
|
||||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||||
final valueList = currentList.intersection(packageNameList);
|
final valueList = currentList.intersection(packageNameList);
|
||||||
@@ -452,21 +505,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
value: valueList.contains(package.packageName),
|
value: valueList.contains(package.packageName),
|
||||||
isActive: isAccessControl,
|
isActive: isAccessControl,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == true) {
|
_handleSelected(
|
||||||
valueList.add(package.packageName);
|
ref,
|
||||||
} else {
|
valueList,
|
||||||
valueList.remove(package.packageName);
|
package,
|
||||||
}
|
value,
|
||||||
final config = globalState.appController.config;
|
);
|
||||||
if (accessControlMode == AccessControlMode.acceptSelected) {
|
|
||||||
config.accessControl = config.accessControl.copyWith(
|
|
||||||
acceptList: valueList,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
config.accessControl = config.accessControl.copyWith(
|
|
||||||
rejectList: valueList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -487,14 +531,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccessControlWidget extends StatelessWidget {
|
class AccessControlPanel extends ConsumerStatefulWidget {
|
||||||
final BuildContext context;
|
const AccessControlPanel({
|
||||||
|
|
||||||
const AccessControlWidget({
|
|
||||||
super.key,
|
super.key,
|
||||||
required this.context,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _AccessControlPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
|
||||||
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
||||||
@@ -539,9 +585,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, AccessControlMode>(
|
child: Consumer(
|
||||||
selector: (_, config) => config.accessControl.mode,
|
builder: (_, ref, __) {
|
||||||
builder: (_, accessControlMode, __) {
|
final accessControlMode = ref.watch(
|
||||||
|
vpnSettingProvider.select((state) => state.accessControl.mode),
|
||||||
|
);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@@ -553,10 +601,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isSelected: accessControlMode == item,
|
isSelected: accessControlMode == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
config.accessControl = config.accessControl.copyWith(
|
(state) => state.copyWith.accessControl(
|
||||||
mode: item,
|
mode: item,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -575,9 +624,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, AccessSortType>(
|
child: Consumer(
|
||||||
selector: (_, config) => config.accessControl.sort,
|
builder: (_, ref, __) {
|
||||||
builder: (_, accessSortType, __) {
|
final accessSortType = ref.watch(
|
||||||
|
vpnSettingProvider.select((state) => state.accessControl.sort),
|
||||||
|
);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@@ -589,10 +640,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isSelected: accessSortType == item,
|
isSelected: accessSortType == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
config.accessControl = config.accessControl.copyWith(
|
(state) => state.copyWith.accessControl(
|
||||||
sort: item,
|
sort: item,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -611,9 +663,12 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, bool>(
|
child: Consumer(
|
||||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
builder: (_, ref, __) {
|
||||||
builder: (_, isFilterSystemApp, __) {
|
final isFilterSystemApp = ref.watch(
|
||||||
|
vpnSettingProvider
|
||||||
|
.select((state) => state.accessControl.isFilterSystemApp),
|
||||||
|
);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@@ -622,10 +677,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
_getTextWithIsFilterSystemApp(item),
|
_getTextWithIsFilterSystemApp(item),
|
||||||
isSelected: isFilterSystemApp == item,
|
isSelected: isFilterSystemApp == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
config.accessControl = config.accessControl.copyWith(
|
(state) => state.copyWith.accessControl(
|
||||||
isFilterSystemApp: item,
|
isFilterSystemApp: item,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -637,63 +693,35 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_intelligentSelected() async {
|
|
||||||
final appState = globalState.appController.appState;
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
final accessControl = config.accessControl;
|
|
||||||
final packageNames = appState.packages
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
|
||||||
)
|
|
||||||
.map((item) => item.packageName);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
final commonScaffoldState = context.commonScaffoldState;
|
|
||||||
if (commonScaffoldState?.mounted != true) return;
|
|
||||||
final selectedPackageNames =
|
|
||||||
(await commonScaffoldState?.loadingRun<List<String>>(
|
|
||||||
() async {
|
|
||||||
return await app?.getChinaPackageNames() ?? [];
|
|
||||||
},
|
|
||||||
))
|
|
||||||
?.toSet() ??
|
|
||||||
{};
|
|
||||||
final acceptList = packageNames
|
|
||||||
.where((item) => !selectedPackageNames.contains(item))
|
|
||||||
.toList();
|
|
||||||
final rejectList = packageNames
|
|
||||||
.where((item) => selectedPackageNames.contains(item))
|
|
||||||
.toList();
|
|
||||||
config.accessControl = accessControl.copyWith(
|
|
||||||
acceptList: acceptList,
|
|
||||||
rejectList: rejectList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_copyToClipboard() async {
|
_copyToClipboard() async {
|
||||||
await globalState.safeRun(() {
|
await globalState.safeRun(() {
|
||||||
final data = globalState.appController.config.accessControl.toJson();
|
final data = globalState.config.vpnProps.accessControl.toJson();
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text: json.encode(data),
|
text: json.encode(data),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (!context.mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pasteToClipboard() async {
|
_pasteToClipboard() async {
|
||||||
await globalState.safeRun(() async {
|
await globalState.safeRun(
|
||||||
final config = globalState.appController.config;
|
() async {
|
||||||
final data = await Clipboard.getData('text/plain');
|
final data = await Clipboard.getData('text/plain');
|
||||||
final text = data?.text;
|
final text = data?.text;
|
||||||
if (text == null) return;
|
if (text == null) return;
|
||||||
config.accessControl = AccessControl.fromJson(
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
json.decode(text),
|
(state) => state.copyWith(
|
||||||
);
|
accessControl: AccessControl.fromJson(
|
||||||
});
|
json.decode(text),
|
||||||
if (!context.mounted) return;
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,7 +740,9 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
CommonChip(
|
CommonChip(
|
||||||
avatar: const Icon(Icons.auto_awesome),
|
avatar: const Icon(Icons.auto_awesome),
|
||||||
label: appLocalizations.intelligentSelected,
|
label: appLocalizations.intelligentSelected,
|
||||||
onPressed: _intelligentSelected,
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(1);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
CommonChip(
|
CommonChip(
|
||||||
avatar: const Icon(Icons.paste),
|
avatar: const Icon(Icons.paste),
|
||||||
|
|||||||
@@ -1,61 +1,258 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class CloseConnectionsSwitch extends StatelessWidget {
|
class CloseConnectionsItem extends ConsumerWidget {
|
||||||
const CloseConnectionsSwitch({super.key});
|
const CloseConnectionsItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final closeConnections = ref.watch(
|
||||||
selector: (_, config) => config.appSetting.closeConnections,
|
appSettingProvider.select((state) => state.closeConnections),
|
||||||
builder: (_, closeConnections, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.autoCloseConnections),
|
title: Text(appLocalizations.autoCloseConnections),
|
||||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: closeConnections,
|
value: closeConnections,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
final config = globalState.appController.config;
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
config.appSetting = config.appSetting.copyWith(
|
(state) => state.copyWith(
|
||||||
closeConnections: value,
|
closeConnections: value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsageSwitch extends StatelessWidget {
|
class UsageItem extends ConsumerWidget {
|
||||||
const UsageSwitch({super.key});
|
const UsageItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final onlyStatisticsProxy = ref.watch(
|
||||||
selector: (_, config) => config.appSetting.onlyProxy,
|
appSettingProvider.select((state) => state.onlyStatisticsProxy),
|
||||||
builder: (_, onlyProxy, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: onlyProxy,
|
value: onlyStatisticsProxy,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final config = globalState.appController.config;
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
config.appSetting = config.appSetting.copyWith(
|
(state) => state.copyWith(
|
||||||
onlyProxy: value,
|
onlyStatisticsProxy: value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MinimizeItem extends ConsumerWidget {
|
||||||
|
const MinimizeItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final minimizeOnExit = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.minimizeOnExit),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.minimizeOnExit),
|
||||||
|
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: minimizeOnExit,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
minimizeOnExit: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoLaunchItem extends ConsumerWidget {
|
||||||
|
const AutoLaunchItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final autoLaunch = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.autoLaunch),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.autoLaunch),
|
||||||
|
subtitle: Text(appLocalizations.autoLaunchDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: autoLaunch,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
autoLaunch: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SilentLaunchItem extends ConsumerWidget {
|
||||||
|
const SilentLaunchItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final silentLaunch = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.silentLaunch),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.silentLaunch),
|
||||||
|
subtitle: Text(appLocalizations.silentLaunchDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: silentLaunch,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
silentLaunch: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoRunItem extends ConsumerWidget {
|
||||||
|
const AutoRunItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final autoRun = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.autoRun),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.autoRun),
|
||||||
|
subtitle: Text(appLocalizations.autoRunDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: autoRun,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
autoRun: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiddenItem extends ConsumerWidget {
|
||||||
|
const HiddenItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final hidden = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.hidden),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.exclude),
|
||||||
|
subtitle: Text(appLocalizations.excludeDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: hidden,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
hidden: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimateTabItem extends ConsumerWidget {
|
||||||
|
const AnimateTabItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isAnimateToPage = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.isAnimateToPage),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.tabAnimation),
|
||||||
|
subtitle: Text(appLocalizations.tabAnimationDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: isAnimateToPage,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
isAnimateToPage: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenLogsItem extends ConsumerWidget {
|
||||||
|
const OpenLogsItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final openLogs = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.openLogs),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.logcat),
|
||||||
|
subtitle: Text(appLocalizations.logcatDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: openLogs,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
openLogs: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoCheckUpdateItem extends ConsumerWidget {
|
||||||
|
const AutoCheckUpdateItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final autoCheckUpdate = ref.watch(
|
||||||
|
appSettingProvider.select((state) => state.autoCheckUpdate),
|
||||||
|
);
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.autoCheckUpdate),
|
||||||
|
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: autoCheckUpdate,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
autoCheckUpdate: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,156 +268,20 @@ class ApplicationSettingFragment extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> items = [
|
List<Widget> items = [
|
||||||
Selector<Config, bool>(
|
MinimizeItem(),
|
||||||
selector: (_, config) => config.appSetting.minimizeOnExit,
|
if (system.isDesktop) ...[
|
||||||
builder: (_, isMinimizeOnExit, child) {
|
AutoLaunchItem(),
|
||||||
return ListItem.switchItem(
|
SilentLaunchItem(),
|
||||||
title: Text(appLocalizations.minimizeOnExit),
|
],
|
||||||
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
AutoRunItem(),
|
||||||
delegate: SwitchDelegate(
|
if (Platform.isAndroid) ...[
|
||||||
value: isMinimizeOnExit,
|
HiddenItem(),
|
||||||
onChanged: (bool value) {
|
AnimateTabItem(),
|
||||||
final config = context.read<Config>();
|
],
|
||||||
config.appSetting = config.appSetting.copyWith(
|
OpenLogsItem(),
|
||||||
minimizeOnExit: value,
|
CloseConnectionsItem(),
|
||||||
);
|
UsageItem(),
|
||||||
},
|
AutoCheckUpdateItem(),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (system.isDesktop)
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.autoLaunch,
|
|
||||||
builder: (_, autoLaunch, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.autoLaunch),
|
|
||||||
subtitle: Text(appLocalizations.autoLaunchDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: autoLaunch,
|
|
||||||
onChanged: (bool value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
autoLaunch: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (system.isDesktop)
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.silentLaunch,
|
|
||||||
builder: (_, silentLaunch, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.silentLaunch),
|
|
||||||
subtitle: Text(appLocalizations.silentLaunchDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: silentLaunch,
|
|
||||||
onChanged: (bool value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
silentLaunch: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.autoRun,
|
|
||||||
builder: (_, autoRun, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.autoRun),
|
|
||||||
subtitle: Text(appLocalizations.autoRunDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: autoRun,
|
|
||||||
onChanged: (bool value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
autoRun: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (Platform.isAndroid)
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.hidden,
|
|
||||||
builder: (_, isExclude, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.exclude),
|
|
||||||
subtitle: Text(appLocalizations.excludeDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: isExclude,
|
|
||||||
onChanged: (value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
hidden: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (Platform.isAndroid)
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.isAnimateToPage,
|
|
||||||
builder: (_, isAnimateToPage, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.tabAnimation),
|
|
||||||
subtitle: Text(appLocalizations.tabAnimationDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: isAnimateToPage,
|
|
||||||
onChanged: (value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
isAnimateToPage: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.openLogs,
|
|
||||||
builder: (_, openLogs, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.logcat),
|
|
||||||
subtitle: Text(appLocalizations.logcatDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: openLogs,
|
|
||||||
onChanged: (bool value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
openLogs: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const CloseConnectionsSwitch(),
|
|
||||||
const UsageSwitch(),
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.appSetting.autoCheckUpdate,
|
|
||||||
builder: (_, autoCheckUpdate, child) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.autoCheckUpdate),
|
|
||||||
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: autoCheckUpdate,
|
|
||||||
onChanged: (bool value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
autoCheckUpdate: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/common/dav_client.dart';
|
import 'package:fl_clash/common/dav_client.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/fade_box.dart';
|
import 'package:fl_clash/widgets/fade_box.dart';
|
||||||
import 'package:fl_clash/widgets/list.dart';
|
import 'package:fl_clash/widgets/list.dart';
|
||||||
import 'package:fl_clash/widgets/text.dart';
|
import 'package:fl_clash/widgets/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class BackupAndRecovery extends StatelessWidget {
|
class BackupAndRecovery extends ConsumerWidget {
|
||||||
const BackupAndRecovery({super.key});
|
const BackupAndRecovery({super.key});
|
||||||
|
|
||||||
_showAddWebDAV(DAV? dav) async {
|
_showAddWebDAV(DAV? dav) async {
|
||||||
@@ -121,139 +122,140 @@ class BackupAndRecovery extends StatelessWidget {
|
|||||||
_recoveryOnLocal(context, recoveryOption);
|
_recoveryOnLocal(context, recoveryOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
_handleChange(String? value, WidgetRef ref) {
|
||||||
Widget build(BuildContext context) {
|
if (value == null) {
|
||||||
return Selector<Config, DAV?>(
|
return;
|
||||||
selector: (_, config) => config.dav,
|
}
|
||||||
builder: (_, dav, __) {
|
ref.read(appDAVSettingProvider.notifier).updateState(
|
||||||
final client = dav != null ? DAVClient(dav) : null;
|
(state) => state?.copyWith(
|
||||||
return ListView(
|
fileName: value,
|
||||||
children: [
|
),
|
||||||
ListHeader(title: appLocalizations.remote),
|
|
||||||
if (dav == null)
|
|
||||||
ListItem(
|
|
||||||
leading: const Icon(Icons.account_box),
|
|
||||||
title: Text(appLocalizations.noInfo),
|
|
||||||
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
|
||||||
trailing: FilledButton.tonal(
|
|
||||||
onPressed: () {
|
|
||||||
_showAddWebDAV(dav);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
appLocalizations.bind,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
ListItem(
|
|
||||||
leading: const Icon(Icons.account_box),
|
|
||||||
title: TooltipText(
|
|
||||||
text: Text(
|
|
||||||
dav.user,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(appLocalizations.connectivity),
|
|
||||||
FutureBuilder<bool>(
|
|
||||||
future: client!.pingCompleter.future,
|
|
||||||
builder: (_, snapshot) {
|
|
||||||
return Center(
|
|
||||||
child: FadeBox(
|
|
||||||
child: snapshot.connectionState ==
|
|
||||||
ConnectionState.waiting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: snapshot.data == true
|
|
||||||
? Colors.green
|
|
||||||
: Colors.red,
|
|
||||||
),
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: FilledButton.tonal(
|
|
||||||
onPressed: () {
|
|
||||||
_showAddWebDAV(dav);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
appLocalizations.edit,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
ListItem.input(
|
|
||||||
title: Text(appLocalizations.file),
|
|
||||||
subtitle: Text(dav.fileName),
|
|
||||||
delegate: InputDelegate(
|
|
||||||
title: appLocalizations.file,
|
|
||||||
value: dav.fileName,
|
|
||||||
resetValue: defaultDavFileName,
|
|
||||||
onChanged: (String? value) {
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
globalState.appController.config.dav =
|
|
||||||
globalState.appController.config.dav?.copyWith(
|
|
||||||
fileName: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListItem(
|
|
||||||
onTap: () {
|
|
||||||
_backupOnWebDAV(context, client);
|
|
||||||
},
|
|
||||||
title: Text(appLocalizations.backup),
|
|
||||||
subtitle: Text(appLocalizations.remoteBackupDesc),
|
|
||||||
),
|
|
||||||
ListItem(
|
|
||||||
onTap: () {
|
|
||||||
_handleRecoveryOnWebDAV(context, client);
|
|
||||||
},
|
|
||||||
title: Text(appLocalizations.recovery),
|
|
||||||
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
ListHeader(title: appLocalizations.local),
|
|
||||||
ListItem(
|
|
||||||
onTap: () {
|
|
||||||
_backupOnLocal(context);
|
|
||||||
},
|
|
||||||
title: Text(appLocalizations.backup),
|
|
||||||
subtitle: Text(appLocalizations.localBackupDesc),
|
|
||||||
),
|
|
||||||
ListItem(
|
|
||||||
onTap: () {
|
|
||||||
_handleRecoveryOnLocal(context);
|
|
||||||
},
|
|
||||||
title: Text(appLocalizations.recovery),
|
|
||||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final dav = ref.watch(appDAVSettingProvider);
|
||||||
|
final client = dav != null ? DAVClient(dav) : null;
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
ListHeader(title: appLocalizations.remote),
|
||||||
|
if (dav == null)
|
||||||
|
ListItem(
|
||||||
|
leading: const Icon(Icons.account_box),
|
||||||
|
title: Text(appLocalizations.noInfo),
|
||||||
|
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
||||||
|
trailing: FilledButton.tonal(
|
||||||
|
onPressed: () {
|
||||||
|
_showAddWebDAV(dav);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
appLocalizations.bind,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
ListItem(
|
||||||
|
leading: const Icon(Icons.account_box),
|
||||||
|
title: TooltipText(
|
||||||
|
text: Text(
|
||||||
|
dav.user,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(appLocalizations.connectivity),
|
||||||
|
FutureBuilder<bool>(
|
||||||
|
future: client!.pingCompleter.future,
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
return Center(
|
||||||
|
child: FadeBox(
|
||||||
|
child: snapshot.connectionState ==
|
||||||
|
ConnectionState.waiting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: snapshot.data == true
|
||||||
|
? Colors.green
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: FilledButton.tonal(
|
||||||
|
onPressed: () {
|
||||||
|
_showAddWebDAV(dav);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
appLocalizations.edit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
ListItem.input(
|
||||||
|
title: Text(appLocalizations.file),
|
||||||
|
subtitle: Text(dav.fileName),
|
||||||
|
delegate: InputDelegate(
|
||||||
|
title: appLocalizations.file,
|
||||||
|
value: dav.fileName,
|
||||||
|
resetValue: defaultDavFileName,
|
||||||
|
onChanged: (value) {
|
||||||
|
_handleChange(value, ref);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListItem(
|
||||||
|
onTap: () {
|
||||||
|
_backupOnWebDAV(context, client);
|
||||||
|
},
|
||||||
|
title: Text(appLocalizations.backup),
|
||||||
|
subtitle: Text(appLocalizations.remoteBackupDesc),
|
||||||
|
),
|
||||||
|
ListItem(
|
||||||
|
onTap: () {
|
||||||
|
_handleRecoveryOnWebDAV(context, client);
|
||||||
|
},
|
||||||
|
title: Text(appLocalizations.recovery),
|
||||||
|
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ListHeader(title: appLocalizations.local),
|
||||||
|
ListItem(
|
||||||
|
onTap: () {
|
||||||
|
_backupOnLocal(context);
|
||||||
|
},
|
||||||
|
title: Text(appLocalizations.backup),
|
||||||
|
subtitle: Text(appLocalizations.localBackupDesc),
|
||||||
|
),
|
||||||
|
ListItem(
|
||||||
|
onTap: () {
|
||||||
|
_handleRecoveryOnLocal(context);
|
||||||
|
},
|
||||||
|
title: Text(appLocalizations.recovery),
|
||||||
|
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,16 +304,16 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebDAVFormDialog extends StatefulWidget {
|
class WebDAVFormDialog extends ConsumerStatefulWidget {
|
||||||
final DAV? dav;
|
final DAV? dav;
|
||||||
|
|
||||||
const WebDAVFormDialog({super.key, this.dav});
|
const WebDAVFormDialog({super.key, this.dav});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
|
||||||
late TextEditingController uriController;
|
late TextEditingController uriController;
|
||||||
late TextEditingController userController;
|
late TextEditingController userController;
|
||||||
late TextEditingController passwordController;
|
late TextEditingController passwordController;
|
||||||
@@ -328,7 +330,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
|||||||
|
|
||||||
_submit() {
|
_submit() {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
globalState.appController.config.dav = DAV(
|
ref.read(appDAVSettingProvider.notifier).value = DAV(
|
||||||
uri: uriController.text,
|
uri: uriController.text,
|
||||||
user: userController.text,
|
user: userController.text,
|
||||||
password: passwordController.text,
|
password: passwordController.text,
|
||||||
@@ -337,7 +339,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_delete() {
|
_delete() {
|
||||||
globalState.appController.config.dav = null;
|
ref.read(appDAVSettingProvider.notifier).value = null;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,208 +1,172 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class OverrideItem extends StatelessWidget {
|
class OverrideItem extends ConsumerWidget {
|
||||||
const OverrideItem({super.key});
|
const OverrideItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final override = ref.watch(overrideDnsProvider);
|
||||||
selector: (_, config) => config.overrideDns,
|
return ListItem.switchItem(
|
||||||
builder: (_, override, __) {
|
title: Text(appLocalizations.overrideDns),
|
||||||
return ListItem.switchItem(
|
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||||
title: Text(appLocalizations.overrideDns),
|
delegate: SwitchDelegate(
|
||||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
value: override,
|
||||||
delegate: SwitchDelegate(
|
onChanged: (bool value) async {
|
||||||
value: override,
|
ref.read(overrideDnsProvider.notifier).value = value;
|
||||||
onChanged: (bool value) async {
|
},
|
||||||
final config = globalState.appController.config;
|
),
|
||||||
config.overrideDns = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusItem extends StatelessWidget {
|
class StatusItem extends ConsumerWidget {
|
||||||
const StatusItem({super.key});
|
const StatusItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final enable =
|
||||||
selector: (_, clashConfig) => clashConfig.dns.enable,
|
ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
|
||||||
builder: (_, enable, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.status),
|
||||||
title: Text(appLocalizations.status),
|
subtitle: Text(appLocalizations.statusDesc),
|
||||||
subtitle: Text(appLocalizations.statusDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: enable,
|
||||||
value: enable,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
.read(patchClashConfigProvider.notifier)
|
||||||
final dns = clashConfig.dns;
|
.updateState((state) => state.copyWith.dns(enable: value));
|
||||||
clashConfig.dns = dns.copyWith(
|
},
|
||||||
enable: value,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreferH3Item extends StatelessWidget {
|
class PreferH3Item extends ConsumerWidget {
|
||||||
const PreferH3Item({super.key});
|
const PreferH3Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final preferH3 = ref
|
||||||
selector: (_, clashConfig) => clashConfig.dns.preferH3,
|
.watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
|
||||||
builder: (_, preferH3, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: const Text("PreferH3"),
|
||||||
title: const Text("PreferH3"),
|
subtitle: Text(appLocalizations.preferH3Desc),
|
||||||
subtitle: Text(appLocalizations.preferH3Desc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: preferH3,
|
||||||
value: preferH3,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
.read(patchClashConfigProvider.notifier)
|
||||||
final dns = clashConfig.dns;
|
.updateState((state) => state.copyWith.dns(preferH3: value));
|
||||||
clashConfig.dns = dns.copyWith(
|
},
|
||||||
preferH3: value,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IPv6Item extends StatelessWidget {
|
class IPv6Item extends ConsumerWidget {
|
||||||
const IPv6Item({super.key});
|
const IPv6Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final ipv6 = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.ipv6,
|
patchClashConfigProvider.select((state) => state.dns.ipv6),
|
||||||
builder: (_, ipv6, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: const Text("IPv6"),
|
title: const Text("IPv6"),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: ipv6,
|
value: ipv6,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(ipv6: value));
|
||||||
ipv6: value,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RespectRulesItem extends StatelessWidget {
|
class RespectRulesItem extends ConsumerWidget {
|
||||||
const RespectRulesItem({super.key});
|
const RespectRulesItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final respectRules = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.respectRules,
|
patchClashConfigProvider.select((state) => state.dns.respectRules),
|
||||||
builder: (_, respectRules, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.respectRules),
|
title: Text(appLocalizations.respectRules),
|
||||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: respectRules,
|
value: respectRules,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(respectRules: value));
|
||||||
respectRules: value,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DnsModeItem extends StatelessWidget {
|
class DnsModeItem extends ConsumerWidget {
|
||||||
const DnsModeItem({super.key});
|
const DnsModeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, DnsMode>(
|
final enhancedMode = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
|
patchClashConfigProvider.select((state) => state.dns.enhancedMode),
|
||||||
builder: (_, enhancedMode, __) {
|
);
|
||||||
return ListItem<DnsMode>.options(
|
return ListItem<DnsMode>.options(
|
||||||
title: Text(appLocalizations.dnsMode),
|
title: Text(appLocalizations.dnsMode),
|
||||||
subtitle: Text(enhancedMode.name),
|
subtitle: Text(enhancedMode.name),
|
||||||
delegate: OptionsDelegate(
|
delegate: OptionsDelegate(
|
||||||
title: appLocalizations.dnsMode,
|
title: appLocalizations.dnsMode,
|
||||||
options: DnsMode.values,
|
options: DnsMode.values,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(enhancedMode: value);
|
.updateState((state) => state.copyWith.dns(enhancedMode: value));
|
||||||
},
|
},
|
||||||
textBuilder: (dnsMode) => dnsMode.name,
|
textBuilder: (dnsMode) => dnsMode.name,
|
||||||
value: enhancedMode,
|
value: enhancedMode,
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeIpRangeItem extends StatelessWidget {
|
class FakeIpRangeItem extends ConsumerWidget {
|
||||||
const FakeIpRangeItem({super.key});
|
const FakeIpRangeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, String>(
|
final fakeIpRange = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
|
patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
|
||||||
builder: (_, fakeIpRange, __) {
|
);
|
||||||
return ListItem.input(
|
return ListItem.input(
|
||||||
title: Text(appLocalizations.fakeipRange),
|
title: Text(appLocalizations.fakeipRange),
|
||||||
subtitle: Text(fakeIpRange),
|
subtitle: Text(fakeIpRange),
|
||||||
delegate: InputDelegate(
|
delegate: InputDelegate(
|
||||||
title: appLocalizations.fakeipRange,
|
title: appLocalizations.fakeipRange,
|
||||||
value: fakeIpRange,
|
value: fakeIpRange,
|
||||||
onChanged: (String? value) {
|
onChanged: (String? value) {
|
||||||
if (value != null) {
|
if (value == null) {
|
||||||
try {
|
return;
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
}
|
||||||
clashConfig.dns = clashConfig.dns.copyWith(
|
ref
|
||||||
fakeIpRange: value,
|
.read(patchClashConfigProvider.notifier)
|
||||||
);
|
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
|
||||||
} catch (e) {
|
},
|
||||||
globalState.showMessage(
|
),
|
||||||
title: appLocalizations.fakeipRange,
|
|
||||||
message: TextSpan(
|
|
||||||
text: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,20 +181,22 @@ class FakeIpFilterItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.fakeipFilter,
|
title: appLocalizations.fakeipFilter,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
final fakeIpFilter = ref.watch(
|
||||||
builder: (_, fakeIpFilter, __) {
|
patchClashConfigProvider
|
||||||
|
.select((state) => state.dns.fakeIpFilter),
|
||||||
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.fakeipFilter,
|
title: appLocalizations.fakeipFilter,
|
||||||
items: fakeIpFilter,
|
items: fakeIpFilter,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items){
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(
|
||||||
fakeIpFilter: List.from(items),
|
fakeIpFilter: List.from(items),
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -252,24 +218,24 @@ class DefaultNameserverItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.defaultNameserver,
|
title: appLocalizations.defaultNameserver,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
final defaultNameserver = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, defaultNameserver, __) {
|
.select((state) => state.dns.defaultNameserver),
|
||||||
return ListPage(
|
);
|
||||||
title: appLocalizations.defaultNameserver,
|
return ListPage(
|
||||||
items: defaultNameserver,
|
title: appLocalizations.defaultNameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
items: defaultNameserver,
|
||||||
onChange: (items){
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.dns = dns.copyWith(
|
(state) => state.copyWith.dns(
|
||||||
defaultNameserver: List.from(items),
|
defaultNameserver: List.from(items),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
}),
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -287,78 +253,71 @@ class NameserverItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
title: appLocalizations.nameserver,
|
title: appLocalizations.nameserver,
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
final nameserver = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider.select((state) => state.dns.nameserver),
|
||||||
builder: (_, nameserver, __) {
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: "域名服务器",
|
title: "域名服务器",
|
||||||
items: nameserver,
|
items: nameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items){
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
nameserver: List.from(items),
|
||||||
nameserver: List.from(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UseHostsItem extends StatelessWidget {
|
class UseHostsItem extends ConsumerWidget {
|
||||||
const UseHostsItem({super.key});
|
const UseHostsItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final useHosts = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.useHosts,
|
patchClashConfigProvider.select((state) => state.dns.useHosts),
|
||||||
builder: (_, useHosts, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.useHosts),
|
title: Text(appLocalizations.useHosts),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: useHosts,
|
value: useHosts,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(useHosts: value));
|
||||||
useHosts: value,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UseSystemHostsItem extends StatelessWidget {
|
class UseSystemHostsItem extends ConsumerWidget {
|
||||||
const UseSystemHostsItem({super.key});
|
const UseSystemHostsItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final useSystemHosts = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
|
patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
|
||||||
builder: (_, useSystemHosts, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.useSystemHosts),
|
title: Text(appLocalizations.useSystemHosts),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: useSystemHosts,
|
value: useSystemHosts,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(
|
||||||
useSystemHosts: value,
|
useSystemHosts: value,
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,26 +333,25 @@ class NameserverPolicyItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.nameserverPolicy,
|
title: appLocalizations.nameserverPolicy,
|
||||||
widget: Selector<ClashConfig, Map<String, String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
final nameserverPolicy = ref.watch(
|
||||||
shouldRebuild: (prev, next) =>
|
patchClashConfigProvider
|
||||||
!const MapEquality<String, String>().equals(prev, next),
|
.select((state) => state.dns.nameserverPolicy),
|
||||||
builder: (_, nameserverPolicy, __) {
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.nameserverPolicy,
|
title: appLocalizations.nameserverPolicy,
|
||||||
items: nameserverPolicy.entries,
|
items: nameserverPolicy.entries,
|
||||||
titleBuilder: (item) => Text(item.key),
|
titleBuilder: (item) => Text(item.key),
|
||||||
subtitleBuilder: (item) => Text(item.value),
|
subtitleBuilder: (item) => Text(item.value),
|
||||||
onChange: (items){
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
nameserverPolicy: Map.fromEntries(items),
|
||||||
nameserverPolicy: Map.fromEntries(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -411,20 +369,22 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.proxyNameserver,
|
title: appLocalizations.proxyNameserver,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
final proxyServerNameserver = ref.watch(
|
||||||
builder: (_, proxyServerNameserver, __) {
|
patchClashConfigProvider
|
||||||
|
.select((state) => state.dns.proxyServerNameserver),
|
||||||
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.proxyNameserver,
|
title: appLocalizations.proxyNameserver,
|
||||||
items: proxyServerNameserver,
|
items: proxyServerNameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items){
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
proxyServerNameserver: List.from(items),
|
||||||
proxyServerNameserver: List.from(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -446,93 +406,80 @@ class FallbackItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.fallback,
|
title: appLocalizations.fallback,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
final fallback = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider.select((state) => state.dns.fallback),
|
||||||
builder: (_, fallback, __) {
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.fallback,
|
title: appLocalizations.fallback,
|
||||||
items: fallback,
|
items: fallback,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items){
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
fallback: List.from(items),
|
||||||
fallback: List.from(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoipItem extends StatelessWidget {
|
class GeoipItem extends ConsumerWidget {
|
||||||
const GeoipItem({super.key});
|
const GeoipItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final geoip = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
|
patchClashConfigProvider
|
||||||
builder: (_, geoip, __) {
|
.select((state) => state.dns.fallbackFilter.geoip),
|
||||||
return ListItem.switchItem(
|
);
|
||||||
title: const Text("Geoip"),
|
return ListItem.switchItem(
|
||||||
delegate: SwitchDelegate(
|
title: const Text("Geoip"),
|
||||||
value: geoip,
|
delegate: SwitchDelegate(
|
||||||
onChanged: (bool value) async {
|
value: geoip,
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChanged: (bool value) async {
|
||||||
final dns = clashConfig.dns;
|
ref
|
||||||
clashConfig.dns = dns.copyWith(
|
.read(patchClashConfigProvider.notifier)
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
|
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||||
);
|
geoip: value,
|
||||||
},
|
));
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoipCodeItem extends StatelessWidget {
|
class GeoipCodeItem extends ConsumerWidget {
|
||||||
const GeoipCodeItem({super.key});
|
const GeoipCodeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, String>(
|
final geoipCode = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
|
patchClashConfigProvider
|
||||||
builder: (_, geoipCode, __) {
|
.select((state) => state.dns.fallbackFilter.geoipCode),
|
||||||
return ListItem.input(
|
);
|
||||||
title: Text(appLocalizations.geoipCode),
|
return ListItem.input(
|
||||||
subtitle: Text(geoipCode),
|
title: Text(appLocalizations.geoipCode),
|
||||||
delegate: InputDelegate(
|
subtitle: Text(geoipCode),
|
||||||
title: appLocalizations.geoipCode,
|
delegate: InputDelegate(
|
||||||
value: geoipCode,
|
title: appLocalizations.geoipCode,
|
||||||
onChanged: (String? value) {
|
value: geoipCode,
|
||||||
if (value != null) {
|
onChanged: (String? value) {
|
||||||
try {
|
if (value == null) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
return;
|
||||||
final dns = clashConfig.dns;
|
}
|
||||||
clashConfig.dns = dns.copyWith(
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
(state) => state.copyWith.dns.fallbackFilter(
|
||||||
geoipCode: value,
|
geoipCode: value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
},
|
||||||
globalState.showMessage(
|
),
|
||||||
title: appLocalizations.geoipCode,
|
|
||||||
message: TextSpan(
|
|
||||||
text: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,26 +494,24 @@ class GeositeItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: "Geosite",
|
title: "Geosite",
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
final geosite = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, geosite, __) {
|
.select((state) => state.dns.fallbackFilter.geosite),
|
||||||
return ListPage(
|
);
|
||||||
title: "Geosite",
|
return ListPage(
|
||||||
items: geosite,
|
title: "Geosite",
|
||||||
titleBuilder: (item) => Text(item),
|
items: geosite,
|
||||||
onChange: (items){
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.dns = dns.copyWith(
|
(state) => state.copyWith.dns.fallbackFilter(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
geosite: List.from(items),
|
||||||
geosite: List.from(items),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -583,26 +528,24 @@ class IpcidrItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.ipcidr,
|
title: appLocalizations.ipcidr,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, ___) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
final ipcidr = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, ipcidr, __) {
|
.select((state) => state.dns.fallbackFilter.ipcidr),
|
||||||
return ListPage(
|
);
|
||||||
title: appLocalizations.ipcidr,
|
return ListPage(
|
||||||
items: ipcidr,
|
title: appLocalizations.ipcidr,
|
||||||
titleBuilder: (item) => Text(item),
|
items: ipcidr,
|
||||||
onChange: (items){
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref
|
||||||
clashConfig.dns = dns.copyWith(
|
.read(patchClashConfigProvider.notifier)
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||||
ipcidr: List.from(items),
|
ipcidr: List.from(items),
|
||||||
),
|
));
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -619,26 +562,24 @@ class DomainItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.domain,
|
title: appLocalizations.domain,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
final domain = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, domain, __) {
|
.select((state) => state.dns.fallbackFilter.domain),
|
||||||
return ListPage(
|
);
|
||||||
title: appLocalizations.domain,
|
return ListPage(
|
||||||
items: domain,
|
title: appLocalizations.domain,
|
||||||
titleBuilder: (item) => Text(item),
|
items: domain,
|
||||||
onChange: (items){
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.dns = dns.copyWith(
|
(state) => state.copyWith.dns.fallbackFilter(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
domain: List.from(items),
|
||||||
domain: List.from(items),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -700,25 +641,29 @@ const dnsItems = <Widget>[
|
|||||||
FallbackFilterOptions(),
|
FallbackFilterOptions(),
|
||||||
];
|
];
|
||||||
|
|
||||||
class DnsListView extends StatelessWidget {
|
class DnsListView extends ConsumerWidget {
|
||||||
const DnsListView({super.key});
|
const DnsListView({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
_initActions(BuildContext context, WidgetRef ref) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
title: appLocalizations.reset,
|
title: appLocalizations.reset,
|
||||||
message: TextSpan(
|
message: TextSpan(
|
||||||
text: appLocalizations.resetTip,
|
text: appLocalizations.resetTip,
|
||||||
),
|
),
|
||||||
onTab: () {
|
);
|
||||||
globalState.appController.clashConfig.dns = defaultDns;
|
if (res != true) {
|
||||||
Navigator.of(context).pop();
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
dns: defaultDns,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
tooltip: appLocalizations.reset,
|
tooltip: appLocalizations.reset,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -730,8 +675,8 @@ class DnsListView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
_initActions(context);
|
_initActions(context, ref);
|
||||||
return generateListView(
|
return generateListView(
|
||||||
dnsItems,
|
dnsItems,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,198 +1,190 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class LogLevelItem extends StatelessWidget {
|
class LogLevelItem extends ConsumerWidget {
|
||||||
const LogLevelItem({super.key});
|
const LogLevelItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, LogLevel>(
|
final logLevel =
|
||||||
selector: (_, clashConfig) => clashConfig.logLevel,
|
ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
|
||||||
builder: (_, value, __) {
|
return ListItem<LogLevel>.options(
|
||||||
return ListItem<LogLevel>.options(
|
leading: const Icon(Icons.info_outline),
|
||||||
leading: const Icon(Icons.info_outline),
|
title: Text(appLocalizations.logLevel),
|
||||||
title: Text(appLocalizations.logLevel),
|
subtitle: Text(logLevel.name),
|
||||||
subtitle: Text(value.name),
|
delegate: OptionsDelegate<LogLevel>(
|
||||||
delegate: OptionsDelegate<LogLevel>(
|
title: appLocalizations.logLevel,
|
||||||
title: appLocalizations.logLevel,
|
options: LogLevel.values,
|
||||||
options: LogLevel.values,
|
onChanged: (LogLevel? value) {
|
||||||
onChanged: (LogLevel? value) {
|
if (value == null) {
|
||||||
if (value == null) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.logLevel = value;
|
logLevel: value,
|
||||||
},
|
),
|
||||||
textBuilder: (logLevel) => logLevel.name,
|
);
|
||||||
value: value,
|
},
|
||||||
),
|
textBuilder: (logLevel) => logLevel.name,
|
||||||
);
|
value: logLevel,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UaItem extends StatelessWidget {
|
class UaItem extends ConsumerWidget {
|
||||||
const UaItem({super.key});
|
const UaItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, String?>(
|
final globalUa =
|
||||||
selector: (_, clashConfig) => clashConfig.globalRealUa,
|
ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
|
||||||
builder: (_, value, __) {
|
return ListItem<String?>.options(
|
||||||
return ListItem<String?>.options(
|
leading: const Icon(Icons.computer_outlined),
|
||||||
leading: const Icon(Icons.computer_outlined),
|
title: const Text("UA"),
|
||||||
title: const Text("UA"),
|
subtitle: Text(globalUa ?? appLocalizations.defaultText),
|
||||||
subtitle: Text(value ?? appLocalizations.defaultText),
|
delegate: OptionsDelegate<String?>(
|
||||||
delegate: OptionsDelegate<String?>(
|
title: "UA",
|
||||||
title: "UA",
|
options: [
|
||||||
options: [
|
null,
|
||||||
null,
|
"clash-verge/v1.6.6",
|
||||||
"clash-verge/v1.6.6",
|
"ClashforWindows/0.19.23",
|
||||||
"ClashforWindows/0.19.23",
|
],
|
||||||
],
|
value: globalUa,
|
||||||
value: value,
|
onChanged: (value) {
|
||||||
onChanged: (ua) {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.globalRealUa = ua;
|
globalUa: value,
|
||||||
},
|
),
|
||||||
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
);
|
||||||
),
|
},
|
||||||
);
|
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeepAliveIntervalItem extends StatelessWidget {
|
class KeepAliveIntervalItem extends ConsumerWidget {
|
||||||
const KeepAliveIntervalItem({super.key});
|
const KeepAliveIntervalItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, int>(
|
final keepAliveInterval = ref.watch(
|
||||||
selector: (_, config) => config.keepAliveInterval,
|
patchClashConfigProvider.select((state) => state.keepAliveInterval));
|
||||||
builder: (_, value, __) {
|
return ListItem.input(
|
||||||
return ListItem.input(
|
leading: const Icon(Icons.timer_outlined),
|
||||||
leading: const Icon(Icons.timer_outlined),
|
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
|
||||||
subtitle: Text("$value ${appLocalizations.seconds}"),
|
delegate: InputDelegate(
|
||||||
delegate: InputDelegate(
|
title: appLocalizations.keepAliveIntervalDesc,
|
||||||
title: appLocalizations.keepAliveIntervalDesc,
|
suffixText: appLocalizations.seconds,
|
||||||
suffixText: appLocalizations.seconds,
|
resetValue: "$defaultKeepAliveInterval",
|
||||||
resetValue: "$defaultKeepAliveInterval",
|
value: "$keepAliveInterval",
|
||||||
value: "$value",
|
onChanged: (String? value) {
|
||||||
onChanged: (String? value) {
|
if (value == null) {
|
||||||
if (value != null) {
|
return;
|
||||||
try {
|
}
|
||||||
final intValue = int.parse(value);
|
globalState.safeRun(
|
||||||
if (intValue <= 0) {
|
() {
|
||||||
throw "Invalid keepAliveInterval";
|
final intValue = int.parse(value);
|
||||||
}
|
if (intValue <= 0) {
|
||||||
globalState.appController.clashConfig.keepAliveInterval =
|
throw "Invalid keepAliveInterval";
|
||||||
intValue;
|
}
|
||||||
} catch (e) {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
globalState.showMessage(
|
(state) => state.copyWith(
|
||||||
title: appLocalizations.keepAliveIntervalDesc,
|
keepAliveInterval: intValue,
|
||||||
message: TextSpan(
|
|
||||||
text: e.toString(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
silence: false,
|
||||||
);
|
title: appLocalizations.keepAliveIntervalDesc,
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestUrlItem extends StatelessWidget {
|
class TestUrlItem extends ConsumerWidget {
|
||||||
const TestUrlItem({super.key});
|
const TestUrlItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, String>(
|
final testUrl =
|
||||||
selector: (_, config) => config.appSetting.testUrl,
|
ref.watch(appSettingProvider.select((state) => state.testUrl));
|
||||||
builder: (_, value, __) {
|
return ListItem.input(
|
||||||
return ListItem.input(
|
leading: const Icon(Icons.timeline),
|
||||||
leading: const Icon(Icons.timeline),
|
title: Text(appLocalizations.testUrl),
|
||||||
title: Text(appLocalizations.testUrl),
|
subtitle: Text(testUrl),
|
||||||
subtitle: Text(value),
|
delegate: InputDelegate(
|
||||||
delegate: InputDelegate(
|
resetValue: defaultTestUrl,
|
||||||
resetValue: defaultTestUrl,
|
title: appLocalizations.testUrl,
|
||||||
title: appLocalizations.testUrl,
|
value: testUrl,
|
||||||
value: value,
|
onChanged: (String? value) {
|
||||||
onChanged: (String? value) {
|
if (value == null) {
|
||||||
if (value != null) {
|
return;
|
||||||
try {
|
}
|
||||||
if (!value.isUrl) {
|
globalState.safeRun(
|
||||||
throw "Invalid url";
|
() {
|
||||||
}
|
if (!value.isUrl) {
|
||||||
final config = globalState.appController.config;
|
throw "Invalid url";
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
testUrl: value,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
globalState.showMessage(
|
|
||||||
title: appLocalizations.testUrl,
|
|
||||||
message: TextSpan(
|
|
||||||
text: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
},
|
(state) => state.copyWith(
|
||||||
),
|
testUrl: value,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
silence: false,
|
||||||
|
title: appLocalizations.testUrl,
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MixedPortItem extends StatelessWidget {
|
class MixedPortItem extends ConsumerWidget {
|
||||||
const MixedPortItem({super.key});
|
const MixedPortItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, int>(
|
final mixedPort =
|
||||||
selector: (_, clashConfig) => clashConfig.mixedPort,
|
ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
|
||||||
builder: (_, value, __) {
|
return ListItem.input(
|
||||||
return ListItem.input(
|
leading: const Icon(Icons.adjust_outlined),
|
||||||
leading: const Icon(Icons.adjust_outlined),
|
title: Text(appLocalizations.proxyPort),
|
||||||
title: Text(appLocalizations.proxyPort),
|
subtitle: Text("$mixedPort"),
|
||||||
subtitle: Text("$value"),
|
delegate: InputDelegate(
|
||||||
delegate: InputDelegate(
|
title: appLocalizations.proxyPort,
|
||||||
title: appLocalizations.proxyPort,
|
value: "$mixedPort",
|
||||||
value: "$value",
|
onChanged: (String? value) {
|
||||||
onChanged: (String? value) {
|
if (value == null) {
|
||||||
if (value != null) {
|
return;
|
||||||
try {
|
}
|
||||||
final mixedPort = int.parse(value);
|
globalState.safeRun(
|
||||||
if (mixedPort < 1024 || mixedPort > 49151) {
|
() {
|
||||||
throw "Invalid port";
|
final mixedPort = int.parse(value);
|
||||||
}
|
if (mixedPort < 1024 || mixedPort > 49151) {
|
||||||
globalState.appController.clashConfig.mixedPort = mixedPort;
|
throw "Invalid port";
|
||||||
} catch (e) {
|
}
|
||||||
globalState.showMessage(
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
title: appLocalizations.proxyPort,
|
(state) => state.copyWith(
|
||||||
message: TextSpan(
|
mixedPort: mixedPort,
|
||||||
text: e.toString(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
resetValue: "$defaultMixedPort",
|
silence: false,
|
||||||
),
|
title: appLocalizations.proxyPort,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
resetValue: "$defaultMixedPort",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,20 +201,21 @@ class HostsItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: "Hosts",
|
title: "Hosts",
|
||||||
widget: Selector<ClashConfig, HostsMap>(
|
widget: Consumer(
|
||||||
selector: (_, clashConfig) => clashConfig.hosts,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) =>
|
final hosts = ref
|
||||||
!const MapEquality<String, String>().equals(prev, next),
|
.watch(patchClashConfigProvider.select((state) => state.hosts));
|
||||||
builder: (_, hosts, ___) {
|
|
||||||
final entries = hosts.entries;
|
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: "Hosts",
|
title: "Hosts",
|
||||||
items: entries,
|
items: hosts.entries,
|
||||||
titleBuilder: (item) => Text(item.key),
|
titleBuilder: (item) => Text(item.key),
|
||||||
subtitleBuilder: (item) => Text(item.value),
|
subtitleBuilder: (item) => Text(item.value),
|
||||||
onChange: (items){
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.hosts = Map.fromEntries(items);
|
(state) => state.copyWith(
|
||||||
|
hosts: Map.fromEntries(items),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -233,190 +226,192 @@ class HostsItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Ipv6Item extends StatelessWidget {
|
class Ipv6Item extends ConsumerWidget {
|
||||||
const Ipv6Item({super.key});
|
const Ipv6Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final ipv6 =
|
||||||
selector: (_, clashConfig) => clashConfig.ipv6,
|
ref.watch(patchClashConfigProvider.select((state) => state.ipv6));
|
||||||
builder: (_, ipv6, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
leading: const Icon(Icons.water_outlined),
|
||||||
leading: const Icon(Icons.water_outlined),
|
title: const Text("IPv6"),
|
||||||
title: const Text("IPv6"),
|
subtitle: Text(appLocalizations.ipv6Desc),
|
||||||
subtitle: Text(appLocalizations.ipv6Desc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: ipv6,
|
||||||
value: ipv6,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.ipv6 = value;
|
ipv6: value,
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllowLanItem extends StatelessWidget {
|
class AllowLanItem extends ConsumerWidget {
|
||||||
const AllowLanItem({super.key});
|
const AllowLanItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final allowLan =
|
||||||
selector: (_, clashConfig) => clashConfig.allowLan,
|
ref.watch(patchClashConfigProvider.select((state) => state.allowLan));
|
||||||
builder: (_, allowLan, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
leading: const Icon(Icons.device_hub),
|
||||||
leading: const Icon(Icons.device_hub),
|
title: Text(appLocalizations.allowLan),
|
||||||
title: Text(appLocalizations.allowLan),
|
subtitle: Text(appLocalizations.allowLanDesc),
|
||||||
subtitle: Text(appLocalizations.allowLanDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: allowLan,
|
||||||
value: allowLan,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final clashConfig = context.read<ClashConfig>();
|
(state) => state.copyWith(
|
||||||
clashConfig.allowLan = value;
|
allowLan: value,
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnifiedDelayItem extends StatelessWidget {
|
class UnifiedDelayItem extends ConsumerWidget {
|
||||||
const UnifiedDelayItem({super.key});
|
const UnifiedDelayItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final unifiedDelay = ref
|
||||||
selector: (_, clashConfig) => clashConfig.unifiedDelay,
|
.watch(patchClashConfigProvider.select((state) => state.unifiedDelay));
|
||||||
builder: (_, unifiedDelay, __) {
|
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.compress_outlined),
|
leading: const Icon(Icons.compress_outlined),
|
||||||
title: Text(appLocalizations.unifiedDelay),
|
title: Text(appLocalizations.unifiedDelay),
|
||||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: unifiedDelay,
|
value: unifiedDelay,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final appController = globalState.appController;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
appController.clashConfig.unifiedDelay = value;
|
(state) => state.copyWith(
|
||||||
},
|
unifiedDelay: value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FindProcessItem extends StatelessWidget {
|
class FindProcessItem extends ConsumerWidget {
|
||||||
const FindProcessItem({super.key});
|
const FindProcessItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final findProcess = ref.watch(patchClashConfigProvider
|
||||||
selector: (_, clashConfig) =>
|
.select((state) => state.findProcessMode == FindProcessMode.always));
|
||||||
clashConfig.findProcessMode == FindProcessMode.always,
|
|
||||||
builder: (_, findProcess, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
leading: const Icon(Icons.polymer_outlined),
|
||||||
leading: const Icon(Icons.polymer_outlined),
|
title: Text(appLocalizations.findProcessMode),
|
||||||
title: Text(appLocalizations.findProcessMode),
|
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: findProcess,
|
||||||
value: findProcess,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.findProcessMode =
|
findProcessMode:
|
||||||
value ? FindProcessMode.always : FindProcessMode.off;
|
value ? FindProcessMode.always : FindProcessMode.off,
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TcpConcurrentItem extends StatelessWidget {
|
class TcpConcurrentItem extends ConsumerWidget {
|
||||||
const TcpConcurrentItem({super.key});
|
const TcpConcurrentItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final tcpConcurrent = ref
|
||||||
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
|
.watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
|
||||||
builder: (_, tcpConcurrent, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
leading: const Icon(Icons.double_arrow_outlined),
|
||||||
leading: const Icon(Icons.double_arrow_outlined),
|
title: Text(appLocalizations.tcpConcurrent),
|
||||||
title: Text(appLocalizations.tcpConcurrent),
|
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: tcpConcurrent,
|
||||||
value: tcpConcurrent,
|
onChanged: (value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.tcpConcurrent = value;
|
tcpConcurrent: value,
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeodataLoaderItem extends StatelessWidget {
|
class GeodataLoaderItem extends ConsumerWidget {
|
||||||
const GeodataLoaderItem({super.key});
|
const GeodataLoaderItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final isMemconservative = ref.watch(patchClashConfigProvider.select(
|
||||||
selector: (_, clashConfig) =>
|
(state) => state.geodataLoader == GeodataLoader.memconservative));
|
||||||
clashConfig.geodataLoader == geodataLoaderMemconservative,
|
return ListItem.switchItem(
|
||||||
builder: (_, memconservative, __) {
|
leading: const Icon(Icons.memory),
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.geodataLoader),
|
||||||
leading: const Icon(Icons.memory),
|
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||||
title: Text(appLocalizations.geodataLoader),
|
delegate: SwitchDelegate(
|
||||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
value: isMemconservative,
|
||||||
delegate: SwitchDelegate(
|
onChanged: (bool value) async {
|
||||||
value: memconservative,
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
onChanged: (bool value) async {
|
(state) => state.copyWith(
|
||||||
final appController = globalState.appController;
|
geodataLoader: value
|
||||||
appController.clashConfig.geodataLoader =
|
? GeodataLoader.memconservative
|
||||||
value ? geodataLoaderMemconservative : geodataLoaderStandard;
|
: GeodataLoader.standard,
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExternalControllerItem extends StatelessWidget {
|
class ExternalControllerItem extends ConsumerWidget {
|
||||||
const ExternalControllerItem({super.key});
|
const ExternalControllerItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final hasExternalController = ref.watch(patchClashConfigProvider.select(
|
||||||
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
|
(state) => state.externalController == ExternalControllerStatus.open));
|
||||||
builder: (_, hasExternalController, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
leading: const Icon(Icons.api_outlined),
|
||||||
leading: const Icon(Icons.api_outlined),
|
title: Text(appLocalizations.externalController),
|
||||||
title: Text(appLocalizations.externalController),
|
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: hasExternalController,
|
||||||
value: hasExternalController,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.externalController =
|
externalController: value
|
||||||
value ? defaultExternalController : '';
|
? ExternalControllerStatus.open
|
||||||
},
|
: ExternalControllerStatus.close,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final generalItems = const [
|
final generalItems = <Widget>[
|
||||||
LogLevelItem(),
|
LogLevelItem(),
|
||||||
UaItem(),
|
UaItem(),
|
||||||
KeepAliveIntervalItem(),
|
if (system.isDesktop) KeepAliveIntervalItem(),
|
||||||
TestUrlItem(),
|
TestUrlItem(),
|
||||||
MixedPortItem(),
|
MixedPortItem(),
|
||||||
HostsItem(),
|
HostsItem(),
|
||||||
|
|||||||
@@ -3,200 +3,185 @@ import 'dart:io';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class VPNItem extends StatelessWidget {
|
class VPNItem extends ConsumerWidget {
|
||||||
const VPNItem({super.key});
|
const VPNItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final enable =
|
||||||
selector: (_, config) => config.vpnProps.enable,
|
ref.watch(vpnSettingProvider.select((state) => state.enable));
|
||||||
builder: (_, enable, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: const Text("VPN"),
|
||||||
title: const Text("VPN"),
|
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: enable,
|
||||||
value: enable,
|
onChanged: (value) async {
|
||||||
onChanged: (value) async {
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
final config = globalState.appController.config;
|
(state) => state.copyWith(
|
||||||
config.vpnProps = config.vpnProps.copyWith(
|
enable: value,
|
||||||
enable: value,
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TUNItem extends StatelessWidget {
|
class TUNItem extends ConsumerWidget {
|
||||||
const TUNItem({super.key});
|
const TUNItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final enable =
|
||||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
|
||||||
builder: (_, enable, __) {
|
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.tun),
|
title: Text(appLocalizations.tun),
|
||||||
subtitle: Text(appLocalizations.tunDesc),
|
subtitle: Text(appLocalizations.tunDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: enable,
|
value: enable,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
(state) => state.copyWith.tun(
|
||||||
enable: value,
|
enable: value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllowBypassItem extends StatelessWidget {
|
class AllowBypassItem extends ConsumerWidget {
|
||||||
const AllowBypassItem({super.key});
|
const AllowBypassItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final allowBypass =
|
||||||
selector: (_, config) => config.vpnProps.allowBypass,
|
ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
|
||||||
builder: (_, allowBypass, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.allowBypass),
|
||||||
title: Text(appLocalizations.allowBypass),
|
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: allowBypass,
|
||||||
value: allowBypass,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
final config = globalState.appController.config;
|
(state) => state.copyWith(
|
||||||
final vpnProps = config.vpnProps;
|
allowBypass: value,
|
||||||
config.vpnProps = vpnProps.copyWith(
|
),
|
||||||
allowBypass: value,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VpnSystemProxyItem extends StatelessWidget {
|
class VpnSystemProxyItem extends ConsumerWidget {
|
||||||
const VpnSystemProxyItem({super.key});
|
const VpnSystemProxyItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final systemProxy =
|
||||||
selector: (_, config) => config.vpnProps.systemProxy,
|
ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
|
||||||
builder: (_, systemProxy, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.systemProxy),
|
||||||
title: Text(appLocalizations.systemProxy),
|
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: systemProxy,
|
||||||
value: systemProxy,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
final config = globalState.appController.config;
|
(state) => state.copyWith(
|
||||||
final vpnProps = config.vpnProps;
|
systemProxy: value,
|
||||||
config.vpnProps = vpnProps.copyWith(
|
),
|
||||||
systemProxy: value,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemProxyItem extends StatelessWidget {
|
class SystemProxyItem extends ConsumerWidget {
|
||||||
const SystemProxyItem({super.key});
|
const SystemProxyItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final systemProxy =
|
||||||
selector: (_, config) => config.networkProps.systemProxy,
|
ref.watch(networkSettingProvider.select((state) => state.systemProxy));
|
||||||
builder: (_, systemProxy, __) {
|
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.systemProxy),
|
title: Text(appLocalizations.systemProxy),
|
||||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: systemProxy,
|
value: systemProxy,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final config = globalState.appController.config;
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
final networkProps = config.networkProps;
|
(state) => state.copyWith(
|
||||||
config.networkProps = networkProps.copyWith(
|
systemProxy: value,
|
||||||
systemProxy: value,
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Ipv6Item extends StatelessWidget {
|
class Ipv6Item extends ConsumerWidget {
|
||||||
const Ipv6Item({super.key});
|
const Ipv6Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
|
||||||
selector: (_, config) => config.vpnProps.ipv6,
|
return ListItem.switchItem(
|
||||||
builder: (_, ipv6, __) {
|
title: const Text("IPv6"),
|
||||||
return ListItem.switchItem(
|
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||||
title: const Text("IPv6"),
|
delegate: SwitchDelegate(
|
||||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
value: ipv6,
|
||||||
delegate: SwitchDelegate(
|
onChanged: (bool value) async {
|
||||||
value: ipv6,
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
onChanged: (bool value) async {
|
(state) => state.copyWith(
|
||||||
final config = globalState.appController.config;
|
ipv6: value,
|
||||||
final vpnProps = config.vpnProps;
|
),
|
||||||
config.vpnProps = vpnProps.copyWith(
|
|
||||||
ipv6: value,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TunStackItem extends StatelessWidget {
|
class TunStackItem extends ConsumerWidget {
|
||||||
const TunStackItem({super.key});
|
const TunStackItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, TunStack>(
|
final stack =
|
||||||
selector: (_, clashConfig) => clashConfig.tun.stack,
|
ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
|
||||||
builder: (_, stack, __) {
|
|
||||||
return ListItem.options(
|
return ListItem.options(
|
||||||
title: Text(appLocalizations.stackMode),
|
title: Text(appLocalizations.stackMode),
|
||||||
subtitle: Text(stack.name),
|
subtitle: Text(stack.name),
|
||||||
delegate: OptionsDelegate<TunStack>(
|
delegate: OptionsDelegate<TunStack>(
|
||||||
value: stack,
|
value: stack,
|
||||||
options: TunStack.values,
|
options: TunStack.values,
|
||||||
textBuilder: (value) => value.name,
|
textBuilder: (value) => value.name,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
(state) => state.copyWith.tun(
|
||||||
stack: value,
|
stack: value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
title: appLocalizations.stackMode,
|
title: appLocalizations.stackMode,
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,26 +189,25 @@ class TunStackItem extends StatelessWidget {
|
|||||||
class BypassDomainItem extends StatelessWidget {
|
class BypassDomainItem extends StatelessWidget {
|
||||||
const BypassDomainItem({super.key});
|
const BypassDomainItem({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
_initActions(BuildContext context, WidgetRef ref) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
title: appLocalizations.reset,
|
title: appLocalizations.reset,
|
||||||
message: TextSpan(
|
message: TextSpan(
|
||||||
text: appLocalizations.resetTip,
|
text: appLocalizations.resetTip,
|
||||||
),
|
),
|
||||||
onTab: () {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.networkProps = config.networkProps.copyWith(
|
|
||||||
bypassDomain: defaultBypassDomain,
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (res != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
bypassDomain: defaultBypassDomain,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
tooltip: appLocalizations.reset,
|
tooltip: appLocalizations.reset,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -243,20 +227,21 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
isBlur: false,
|
isBlur: false,
|
||||||
isScaffold: true,
|
isScaffold: true,
|
||||||
title: appLocalizations.bypassDomain,
|
title: appLocalizations.bypassDomain,
|
||||||
widget: Selector<Config, List<String>>(
|
widget: Consumer(
|
||||||
selector: (_, config) => config.networkProps.bypassDomain,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
_initActions(context, ref);
|
||||||
builder: (context, bypassDomain, __) {
|
final bypassDomain = ref.watch(
|
||||||
_initActions(context);
|
networkSettingProvider.select((state) => state.bypassDomain));
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.bypassDomain,
|
title: appLocalizations.bypassDomain,
|
||||||
items: bypassDomain,
|
items: bypassDomain,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final config = globalState.appController.config;
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
config.networkProps = config.networkProps.copyWith(
|
(state) => state.copyWith(
|
||||||
bypassDomain: List.from(items),
|
bypassDomain: List.from(items),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -267,77 +252,74 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteModeItem extends StatelessWidget {
|
class RouteModeItem extends ConsumerWidget {
|
||||||
const RouteModeItem({super.key});
|
const RouteModeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, RouteMode>(
|
final routeMode =
|
||||||
selector: (_, clashConfig) => clashConfig.routeMode,
|
ref.watch(networkSettingProvider.select((state) => state.routeMode));
|
||||||
builder: (_, value, __) {
|
return ListItem<RouteMode>.options(
|
||||||
return ListItem<RouteMode>.options(
|
title: Text(appLocalizations.routeMode),
|
||||||
title: Text(appLocalizations.routeMode),
|
subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
|
||||||
subtitle: Text(Intl.message("routeMode_${value.name}")),
|
delegate: OptionsDelegate<RouteMode>(
|
||||||
delegate: OptionsDelegate<RouteMode>(
|
title: appLocalizations.routeMode,
|
||||||
title: appLocalizations.routeMode,
|
options: RouteMode.values,
|
||||||
options: RouteMode.values,
|
onChanged: (RouteMode? value) {
|
||||||
onChanged: (RouteMode? value) {
|
if (value == null) {
|
||||||
if (value == null) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.routeMode = value;
|
routeMode: value,
|
||||||
},
|
),
|
||||||
textBuilder: (routeMode) => Intl.message(
|
);
|
||||||
"routeMode_${routeMode.name}",
|
},
|
||||||
),
|
textBuilder: (routeMode) => Intl.message(
|
||||||
value: value,
|
"routeMode_${routeMode.name}",
|
||||||
),
|
),
|
||||||
);
|
value: routeMode,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteAddressItem extends StatelessWidget {
|
class RouteAddressItem extends ConsumerWidget {
|
||||||
const RouteAddressItem({super.key});
|
const RouteAddressItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final bypassPrivate = ref.watch(networkSettingProvider
|
||||||
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
|
.select((state) => state.routeMode == RouteMode.bypassPrivate));
|
||||||
builder: (_, value, child) {
|
if (bypassPrivate) {
|
||||||
if (value) {
|
return Container();
|
||||||
return child!;
|
}
|
||||||
}
|
return ListItem.open(
|
||||||
return Container();
|
title: Text(appLocalizations.routeAddress),
|
||||||
},
|
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||||
child: ListItem.open(
|
delegate: OpenDelegate(
|
||||||
title: Text(appLocalizations.routeAddress),
|
isBlur: false,
|
||||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
isScaffold: true,
|
||||||
delegate: OpenDelegate(
|
title: appLocalizations.routeAddress,
|
||||||
isBlur: false,
|
widget: Consumer(
|
||||||
isScaffold: true,
|
builder: (_, ref, __) {
|
||||||
title: appLocalizations.routeAddress,
|
final routeAddress = ref.watch(patchClashConfigProvider
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
.select((state) => state.tun.routeAddress));
|
||||||
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
|
return ListPage(
|
||||||
shouldRebuild: (prev, next) =>
|
title: appLocalizations.routeAddress,
|
||||||
!stringListEquality.equals(prev, next),
|
items: routeAddress,
|
||||||
builder: (context, routeAddress, __) {
|
titleBuilder: (item) => Text(item),
|
||||||
return ListPage(
|
onChange: (items) {
|
||||||
title: appLocalizations.routeAddress,
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
items: routeAddress,
|
(state) => state.copyWith.tun(
|
||||||
titleBuilder: (item) => Text(item),
|
routeAddress: List.from(items),
|
||||||
onChange: (items) {
|
),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
);
|
||||||
clashConfig.includeRouteAddress =
|
},
|
||||||
Set<String>.from(items).toList();
|
);
|
||||||
},
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
|
||||||
),
|
),
|
||||||
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -349,7 +331,8 @@ final networkItems = [
|
|||||||
...generateSection(
|
...generateSection(
|
||||||
title: "VPN",
|
title: "VPN",
|
||||||
items: [
|
items: [
|
||||||
const SystemProxyItem(),
|
const VpnSystemProxyItem(),
|
||||||
|
const BypassDomainItem(),
|
||||||
const AllowBypassItem(),
|
const AllowBypassItem(),
|
||||||
const Ipv6Item(),
|
const Ipv6Item(),
|
||||||
],
|
],
|
||||||
@@ -373,28 +356,31 @@ final networkItems = [
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
class NetworkListView extends StatelessWidget {
|
class NetworkListView extends ConsumerWidget {
|
||||||
const NetworkListView({super.key});
|
const NetworkListView({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
_initActions(BuildContext context, WidgetRef ref) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
title: appLocalizations.reset,
|
title: appLocalizations.reset,
|
||||||
message: TextSpan(
|
message: TextSpan(
|
||||||
text: appLocalizations.resetTip,
|
text: appLocalizations.resetTip,
|
||||||
),
|
),
|
||||||
onTab: () {
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.config.vpnProps = defaultVpnProps;
|
|
||||||
appController.clashConfig.tun = defaultTun;
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (res != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
|
(state) => defaultVpnProps,
|
||||||
|
);
|
||||||
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
tun: defaultTun,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
tooltip: appLocalizations.reset,
|
tooltip: appLocalizations.reset,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -406,8 +392,8 @@ class NetworkListView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
_initActions(context);
|
_initActions(context, ref);
|
||||||
return generateListView(
|
return generateListView(
|
||||||
networkItems,
|
networkItems,
|
||||||
);
|
);
|
||||||
|
|||||||
150
lib/fragments/connection/connections.dart
Normal file
150
lib/fragments/connection/connections.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'item.dart';
|
||||||
|
|
||||||
|
class ConnectionsFragment extends ConsumerStatefulWidget {
|
||||||
|
const ConnectionsFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConnectionsFragment> createState() =>
|
||||||
|
_ConnectionsFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
|
||||||
|
with PageMixin {
|
||||||
|
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
|
||||||
|
const ConnectionsState(),
|
||||||
|
);
|
||||||
|
final ScrollController _scrollController = ScrollController(
|
||||||
|
keepScrollOffset: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
clashCore.closeConnections();
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateConnections() async {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
timer = Timer(Duration(seconds: 1), () async {
|
||||||
|
_updateConnections();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
ref.listenManual(
|
||||||
|
isCurrentPageProvider(
|
||||||
|
PageLabel.connections,
|
||||||
|
handler: (pageLabel, viewMode) =>
|
||||||
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||||
|
),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next && next == true) {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
_updateConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleBlockConnection(String id) async {
|
||||||
|
clashCore.closeConnection(id);
|
||||||
|
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
_connectionsStateNotifier.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
timer = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<ConnectionsState>(
|
||||||
|
valueListenable: _connectionsStateNotifier,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final connections = state.list;
|
||||||
|
if (connections.isEmpty) {
|
||||||
|
return NullStatus(
|
||||||
|
label: appLocalizations.nullConnectionsDesc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CommonScrollBar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.separated(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final connection = connections[index];
|
||||||
|
return ConnectionItem(
|
||||||
|
key: Key(connection.id),
|
||||||
|
connection: connection,
|
||||||
|
onClick: (value) {
|
||||||
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.block),
|
||||||
|
onPressed: () {
|
||||||
|
_handleBlockConnection(connection.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return const Divider(
|
||||||
|
height: 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: connections.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
lib/fragments/connection/item.dart
Normal file
155
lib/fragments/connection/item.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class FindProcessBuilder extends StatelessWidget {
|
||||||
|
final Widget Function(bool value) builder;
|
||||||
|
|
||||||
|
const FindProcessBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer(
|
||||||
|
builder: (_, ref, __) {
|
||||||
|
final value = ref.watch(
|
||||||
|
patchClashConfigProvider.select(
|
||||||
|
(state) =>
|
||||||
|
state.findProcessMode == FindProcessMode.always &&
|
||||||
|
Platform.isAndroid,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return builder(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionItem extends StatelessWidget {
|
||||||
|
final Connection connection;
|
||||||
|
final Function(String)? onClick;
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
const ConnectionItem({
|
||||||
|
super.key,
|
||||||
|
required this.connection,
|
||||||
|
this.onClick,
|
||||||
|
this.trailing,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
|
||||||
|
return await app?.getPackageIcon(connection.metadata.process);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSourceText(Connection connection) {
|
||||||
|
final metadata = connection.metadata;
|
||||||
|
if (metadata.process.isEmpty) {
|
||||||
|
return connection.start.lastUpdateTimeDesc;
|
||||||
|
}
|
||||||
|
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final title = Text(
|
||||||
|
connection.desc,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
);
|
||||||
|
final subTitle = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_getSourceText(connection),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 6,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
for (final chain in connection.chains)
|
||||||
|
CommonChip(
|
||||||
|
label: chain,
|
||||||
|
onPressed: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
onClick!(chain);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return ListItem(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
title: title,
|
||||||
|
subtitle: subTitle,
|
||||||
|
trailing: trailing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FindProcessBuilder(
|
||||||
|
builder: (bool value) {
|
||||||
|
final leading = value
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
final process = connection.metadata.process;
|
||||||
|
if (process.isEmpty) return;
|
||||||
|
onClick!(process);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
child: FutureBuilder<ImageProvider?>(
|
||||||
|
future: _getPackageIcon(connection),
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
if (!snapshot.hasData && snapshot.data == null) {
|
||||||
|
return Container();
|
||||||
|
} else {
|
||||||
|
return Image(
|
||||||
|
image: snapshot.data!,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return ListItem(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
leading: leading,
|
||||||
|
title: title,
|
||||||
|
subtitle: subTitle,
|
||||||
|
trailing: trailing,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
212
lib/fragments/connection/requests.dart
Normal file
212
lib/fragments/connection/requests.dart
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
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/providers/providers.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'item.dart';
|
||||||
|
|
||||||
|
double _preOffset = 0;
|
||||||
|
|
||||||
|
class RequestsFragment extends ConsumerStatefulWidget {
|
||||||
|
const RequestsFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||||
|
with PageMixin {
|
||||||
|
final _requestsStateNotifier =
|
||||||
|
ValueNotifier<ConnectionsState>(const ConnectionsState());
|
||||||
|
List<Connection> _requests = [];
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController(
|
||||||
|
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||||
|
|
||||||
|
double _currentMaxWidth = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_requestsStateNotifier.value =
|
||||||
|
_requestsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
connections: globalState.appState.requests.list,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listenManual(
|
||||||
|
isCurrentPageProvider(
|
||||||
|
PageLabel.requests,
|
||||||
|
handler: (pageLabel, viewMode) =>
|
||||||
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||||
|
),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next && next == true) {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
ref.listenManual(
|
||||||
|
requestsProvider.select((state) => state.list),
|
||||||
|
(prev, next) {
|
||||||
|
if (!connectionListEquality.equals(prev, next)) {
|
||||||
|
_requests = next;
|
||||||
|
updateRequestsThrottler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calcCacheHeight(Connection item) {
|
||||||
|
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
|
||||||
|
if (cacheHeight != null) {
|
||||||
|
return cacheHeight;
|
||||||
|
}
|
||||||
|
final size = globalState.measure.computeTextSize(
|
||||||
|
Text(
|
||||||
|
item.desc,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
maxWidth: _currentMaxWidth,
|
||||||
|
);
|
||||||
|
final chainsText = item.chains.join("");
|
||||||
|
final length = item.chains.length;
|
||||||
|
final chainSize = globalState.measure.computeTextSize(
|
||||||
|
Text(
|
||||||
|
chainsText,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
|
||||||
|
);
|
||||||
|
final baseHeight = globalState.measure.bodyMediumHeight;
|
||||||
|
final lines = (chainSize.height / baseHeight).round();
|
||||||
|
final computerHeight =
|
||||||
|
size.height + chainSize.height + 24 + 24 * (lines - 1);
|
||||||
|
_cacheDynamicHeightMap.put(item.id, computerHeight);
|
||||||
|
return computerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleTryClearCache(double maxWidth) {
|
||||||
|
if (_currentMaxWidth != maxWidth) {
|
||||||
|
_currentMaxWidth = maxWidth;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_requestsStateNotifier.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
_currentMaxWidth = 0;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequestsThrottler() {
|
||||||
|
throttler.call("request", () {
|
||||||
|
final isEquality = connectionListEquality.equals(
|
||||||
|
_requests,
|
||||||
|
_requestsStateNotifier.value.connections,
|
||||||
|
);
|
||||||
|
if (isEquality) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
connections: _requests,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, duration: commonDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
return FindProcessBuilder(builder: (value) {
|
||||||
|
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||||
|
return ValueListenableBuilder<ConnectionsState>(
|
||||||
|
valueListenable: _requestsStateNotifier,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final connections = state.list;
|
||||||
|
if (connections.isEmpty) {
|
||||||
|
return NullStatus(
|
||||||
|
label: appLocalizations.nullRequestsDesc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final items = connections
|
||||||
|
.map<Widget>(
|
||||||
|
(connection) => ConnectionItem(
|
||||||
|
key: Key(connection.id),
|
||||||
|
connection: connection,
|
||||||
|
onClick: (value) {
|
||||||
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.separated(
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: NotificationListener<ScrollEndNotification>(
|
||||||
|
onNotification: (details) {
|
||||||
|
_preOffset = details.metrics.pixels;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: CommonScrollBar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: NextClampingScrollPhysics(),
|
||||||
|
controller: _scrollController,
|
||||||
|
itemExtentBuilder: (index, __) {
|
||||||
|
final widget = items[index];
|
||||||
|
if (widget.runtimeType == Divider) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final measure = globalState.measure;
|
||||||
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||||
|
final connection = connections[(index / 2).floor()];
|
||||||
|
final height = _calcCacheHeight(connection);
|
||||||
|
return height + bodyMediumHeight + 32;
|
||||||
|
},
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return items[index];
|
||||||
|
},
|
||||||
|
itemCount: items.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class ConnectionsFragment extends StatefulWidget {
|
|
||||||
const ConnectionsFragment({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
|
||||||
final connectionsNotifier =
|
|
||||||
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
|
|
||||||
final ScrollController _scrollController = ScrollController(
|
|
||||||
keepScrollOffset: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
Timer? timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
if (timer != null) {
|
|
||||||
timer?.cancel();
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
timer = Timer.periodic(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
(timer) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_initActions() {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showSearch(
|
|
||||||
context: context,
|
|
||||||
delegate: ConnectionsSearchDelegate(
|
|
||||||
state: connectionsNotifier.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
clashCore.closeConnections();
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete_sweep_outlined),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleBlockConnection(String id) async {
|
|
||||||
clashCore.closeConnection(id);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
timer?.cancel();
|
|
||||||
connectionsNotifier.dispose();
|
|
||||||
_scrollController.dispose();
|
|
||||||
timer = null;
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<AppState, bool?>(
|
|
||||||
selector: (_, appState) =>
|
|
||||||
appState.currentLabel == 'connections' ||
|
|
||||||
appState.viewMode == ViewMode.mobile &&
|
|
||||||
appState.currentLabel == "tools",
|
|
||||||
builder: (_, isCurrent, child) {
|
|
||||||
if (isCurrent == null || isCurrent) {
|
|
||||||
_initActions();
|
|
||||||
}
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: ValueListenableBuilder<ConnectionsAndKeywords>(
|
|
||||||
valueListenable: connectionsNotifier,
|
|
||||||
builder: (_, state, __) {
|
|
||||||
var connections = state.filteredConnections;
|
|
||||||
if (connections.isEmpty) {
|
|
||||||
return NullStatus(
|
|
||||||
label: appLocalizations.nullConnectionsDesc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
connections = connections.reversed.toList();
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
controller: _scrollController,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final connection = connections[index];
|
|
||||||
return ConnectionItem(
|
|
||||||
key: Key(connection.id),
|
|
||||||
connection: connection,
|
|
||||||
onClick: _addKeyword,
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.block),
|
|
||||||
onPressed: () {
|
|
||||||
_handleBlockConnection(connection.id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: connections.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConnectionsSearchDelegate extends SearchDelegate {
|
|
||||||
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
|
|
||||||
|
|
||||||
ConnectionsSearchDelegate({
|
|
||||||
required ConnectionsAndKeywords state,
|
|
||||||
}) : connectionsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
|
|
||||||
|
|
||||||
get state => connectionsNotifier.value;
|
|
||||||
|
|
||||||
List<Connection> get _results {
|
|
||||||
final lowerQuery = query.toLowerCase().trim();
|
|
||||||
return connectionsNotifier.value.filteredConnections.where((request) {
|
|
||||||
final lowerNetwork = request.metadata.network.toLowerCase();
|
|
||||||
final lowerHost = request.metadata.host.toLowerCase();
|
|
||||||
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
|
|
||||||
final lowerProcess = request.metadata.process.toLowerCase();
|
|
||||||
final lowerChains = request.chains.join("").toLowerCase();
|
|
||||||
return lowerNetwork.contains(lowerQuery) ||
|
|
||||||
lowerHost.contains(lowerQuery) ||
|
|
||||||
lowerDestinationIP.contains(lowerQuery) ||
|
|
||||||
lowerProcess.contains(lowerQuery) ||
|
|
||||||
lowerChains.contains(lowerQuery);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleBlockConnection(String id) async {
|
|
||||||
clashCore.closeConnection(id);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget>? buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
close(context, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query = '';
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close(context, null);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildResults(BuildContext context) {
|
|
||||||
return buildSuggestions(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
connectionsNotifier.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: connectionsNotifier,
|
|
||||||
builder: (_, __, ___) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final connection = _results[index];
|
|
||||||
return ConnectionItem(
|
|
||||||
key: Key(connection.id),
|
|
||||||
connection: connection,
|
|
||||||
onClick: _addKeyword,
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.block),
|
|
||||||
onPressed: () {
|
|
||||||
_handleBlockConnection(connection.id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: _results.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,43 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'widgets/start_button.dart';
|
import 'widgets/start_button.dart';
|
||||||
|
|
||||||
class DashboardFragment extends StatefulWidget {
|
class DashboardFragment extends ConsumerStatefulWidget {
|
||||||
const DashboardFragment({super.key});
|
const DashboardFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DashboardFragment> createState() => _DashboardFragmentState();
|
ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardFragmentState extends State<DashboardFragment> {
|
class _DashboardFragmentState extends ConsumerState<DashboardFragment>
|
||||||
|
with PageMixin {
|
||||||
final key = GlobalKey<SuperGridState>();
|
final key = GlobalKey<SuperGridState>();
|
||||||
|
|
||||||
_initScaffold(bool isCurrent) {
|
@override
|
||||||
if (!isCurrent) {
|
initState() {
|
||||||
return;
|
ref.listenManual(
|
||||||
}
|
isCurrentPageProvider(PageLabel.dashboard),
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
(prev, next) {
|
||||||
final commonScaffoldState =
|
if (prev != next && next == true) {
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
initPageState();
|
||||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
}
|
||||||
commonScaffoldState?.actions = [
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
return super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? get floatingActionButton => const StartButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: key.currentState!.addedChildrenNotifier,
|
valueListenable: key.currentState!.addedChildrenNotifier,
|
||||||
builder: (_, addedChildren, child) {
|
builder: (_, addedChildren, child) {
|
||||||
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
});
|
|
||||||
|
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
|
||||||
|
final dashboardWidgets = girdItems
|
||||||
|
.map(
|
||||||
|
(item) => DashboardWidget.getDashboardWidget(item),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActiveBuilder(
|
final dashboardState = ref.watch(dashboardStateProvider);
|
||||||
label: "dashboard",
|
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
|
||||||
builder: (isCurrent, child) {
|
return Align(
|
||||||
_initScaffold(isCurrent);
|
alignment: Alignment.topCenter,
|
||||||
return child!;
|
child: SingleChildScrollView(
|
||||||
},
|
padding: const EdgeInsets.all(16).copyWith(
|
||||||
child: Align(
|
bottom: 88,
|
||||||
alignment: Alignment.topCenter,
|
),
|
||||||
child: SingleChildScrollView(
|
child: SuperGrid(
|
||||||
padding: const EdgeInsets.all(16).copyWith(
|
key: key,
|
||||||
bottom: 88,
|
crossAxisCount: columns,
|
||||||
),
|
crossAxisSpacing: 16,
|
||||||
child: Selector2<AppState, Config, DashboardState>(
|
mainAxisSpacing: 16,
|
||||||
selector: (_, appState, config) => DashboardState(
|
children: [
|
||||||
dashboardWidgets: config.appSetting.dashboardWidgets,
|
...dashboardState.dashboardWidgets
|
||||||
viewWidth: appState.viewWidth,
|
.where(
|
||||||
),
|
(item) => item.platforms.contains(
|
||||||
builder: (_, state, ___) {
|
SupportPlatform.currentPlatform,
|
||||||
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
|
),
|
||||||
return SuperGrid(
|
)
|
||||||
key: key,
|
.map(
|
||||||
crossAxisCount: columns,
|
(item) => item.widget,
|
||||||
crossAxisSpacing: 16,
|
),
|
||||||
mainAxisSpacing: 16,
|
],
|
||||||
children: [
|
onSave: (girdItems) {
|
||||||
...state.dashboardWidgets
|
_handleSave(girdItems, ref);
|
||||||
.where(
|
},
|
||||||
(item) => item.platforms.contains(
|
addedItemsBuilder: (girdItems) {
|
||||||
SupportPlatform.currentPlatform,
|
return DashboardWidget.values
|
||||||
),
|
.where(
|
||||||
)
|
(item) =>
|
||||||
.map(
|
!girdItems.contains(item.widget) &&
|
||||||
(item) => item.widget,
|
item.platforms.contains(
|
||||||
|
SupportPlatform.currentPlatform,
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
onSave: (girdItems) {
|
.map((item) => item.widget)
|
||||||
final dashboardWidgets = girdItems
|
.toList();
|
||||||
.map(
|
},
|
||||||
(item) => DashboardWidget.getDashboardWidget(item),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
dashboardWidgets: dashboardWidgets,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
addedItemsBuilder: (girdItems) {
|
|
||||||
return DashboardWidget.values
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
!girdItems.contains(item.widget) &&
|
|
||||||
item.platforms.contains(
|
|
||||||
SupportPlatform.currentPlatform,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((item) => item.widget)
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class CoreInfo extends StatelessWidget {
|
|
||||||
const CoreInfo({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<AppState, VersionInfo?>(
|
|
||||||
selector: (_, appState) => appState.versionInfo,
|
|
||||||
builder: (_, versionInfo, __) {
|
|
||||||
return CommonCard(
|
|
||||||
onPressed: () {},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.coreInfo,
|
|
||||||
iconData: Icons.memory,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Text(
|
|
||||||
versionInfo?.clashName ?? '',
|
|
||||||
style: context
|
|
||||||
.textTheme
|
|
||||||
.titleMedium
|
|
||||||
?.toSoftBold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Text(
|
|
||||||
versionInfo?.version ?? '',
|
|
||||||
style: context
|
|
||||||
.textTheme
|
|
||||||
.titleLarge
|
|
||||||
?.toSoftBold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/app.dart';
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class IntranetIP extends StatelessWidget {
|
class IntranetIP extends StatelessWidget {
|
||||||
const IntranetIP({super.key});
|
const IntranetIP({super.key});
|
||||||
@@ -28,15 +28,15 @@ class IntranetIP extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: globalState.measure.bodyMediumHeight + 2,
|
height: globalState.measure.bodyMediumHeight + 2,
|
||||||
child: Selector<AppFlowingState, String?>(
|
child: Consumer(
|
||||||
selector: (_, appFlowingState) => appFlowingState.localIp,
|
builder: (_, ref, __) {
|
||||||
builder: (_, value, __) {
|
final localIp = ref.watch(localIpProvider);
|
||||||
return FadeBox(
|
return FadeBox(
|
||||||
child: value != null
|
child: localIp != null
|
||||||
? TooltipText(
|
? TooltipText(
|
||||||
text: Text(
|
text: Text(
|
||||||
value.isNotEmpty
|
localIp.isNotEmpty
|
||||||
? value
|
? localIp
|
||||||
: appLocalizations.noNetwork,
|
: appLocalizations.noNetwork,
|
||||||
style: context.textTheme.bodyMedium?.toLight
|
style: context.textTheme.bodyMedium?.toLight
|
||||||
.adjustSize(1),
|
.adjustSize(1),
|
||||||
@@ -48,7 +48,9 @@ class IntranetIP extends StatelessWidget {
|
|||||||
padding: EdgeInsets.all(2),
|
padding: EdgeInsets.all(2),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
@@ -6,8 +7,9 @@ import 'package:fl_clash/models/common.dart';
|
|||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
final _memoryInfoStateNotifier =
|
final _memoryInfoStateNotifier = ValueNotifier<TrafficValue>(
|
||||||
ValueNotifier<TrafficValue>(TrafficValue(value: 0));
|
TrafficValue(value: 0),
|
||||||
|
);
|
||||||
|
|
||||||
class MemoryInfo extends StatefulWidget {
|
class MemoryInfo extends StatefulWidget {
|
||||||
const MemoryInfo({super.key});
|
const MemoryInfo({super.key});
|
||||||
@@ -22,10 +24,7 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
clashCore.getMemory().then((memory) {
|
_updateMemory();
|
||||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
|
||||||
});
|
|
||||||
_updateMemoryData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -34,16 +33,25 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateMemoryData() {
|
_updateMemory() async {
|
||||||
timer = Timer(Duration(seconds: 2), () async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final memory = await clashCore.getMemory();
|
final rss = ProcessInfo.currentRss;
|
||||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
_memoryInfoStateNotifier.value = TrafficValue(
|
||||||
_updateMemoryData();
|
value: clashLib != null ? rss : await clashCore.getMemory() + rss,
|
||||||
|
);
|
||||||
|
timer = Timer(Duration(seconds: 5), () async {
|
||||||
|
_updateMemory();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final darkenLighter = context.colorScheme.secondaryContainer
|
||||||
|
.blendDarken(context, factor: 0.1)
|
||||||
|
.toLighter;
|
||||||
|
final darken = context.colorScheme.secondaryContainer
|
||||||
|
.blendDarken(context, factor: 0.1);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: getWidgetHeight(2),
|
height: getWidgetHeight(2),
|
||||||
child: CommonCard(
|
child: CommonCard(
|
||||||
@@ -87,17 +95,14 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
|||||||
child: WaveView(
|
child: WaveView(
|
||||||
waveAmplitude: 12.0,
|
waveAmplitude: 12.0,
|
||||||
waveFrequency: 0.35,
|
waveFrequency: 0.35,
|
||||||
waveColor: context.colorScheme.secondaryContainer
|
waveColor: darkenLighter,
|
||||||
.blendDarken(context, factor: 0.1)
|
|
||||||
.toLighter,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: WaveView(
|
child: WaveView(
|
||||||
waveAmplitude: 12.0,
|
waveAmplitude: 12.0,
|
||||||
waveFrequency: 0.9,
|
waveFrequency: 0.9,
|
||||||
waveColor: context.colorScheme.secondaryContainer
|
waveColor: darken,
|
||||||
.blendDarken(context, factor: 0.1),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||||
const NetworkDetectionState(
|
const NetworkDetectionState(
|
||||||
@@ -16,14 +17,14 @@ final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class NetworkDetection extends StatefulWidget {
|
class NetworkDetection extends ConsumerStatefulWidget {
|
||||||
const NetworkDetection({super.key});
|
const NetworkDetection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NetworkDetection> createState() => _NetworkDetectionState();
|
ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||||
bool? _preIsStart;
|
bool? _preIsStart;
|
||||||
Timer? _setTimeoutTimer;
|
Timer? _setTimeoutTimer;
|
||||||
CancelToken? cancelToken;
|
CancelToken? cancelToken;
|
||||||
@@ -31,6 +32,11 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
ref.listenManual(checkIpNumProvider, (prev, next) {
|
||||||
|
if (prev != next) {
|
||||||
|
_startCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,12 +53,13 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_checkIp() async {
|
_checkIp() async {
|
||||||
final appState = globalState.appController.appState;
|
final appState = globalState.appState;
|
||||||
final appFlowingState = globalState.appController.appFlowingState;
|
|
||||||
final isInit = appState.isInit;
|
final isInit = appState.isInit;
|
||||||
if (!isInit) return;
|
if (!isInit) return;
|
||||||
final isStart = appFlowingState.isStart;
|
final isStart = appState.runTime != null;
|
||||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
if (_preIsStart == false &&
|
||||||
|
_preIsStart == isStart &&
|
||||||
|
_networkDetectionState.value.ipInfo != null) return;
|
||||||
_clearSetTimeoutTimer();
|
_clearSetTimeoutTimer();
|
||||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||||
isTesting: true,
|
isTesting: true,
|
||||||
@@ -109,24 +116,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkIpContainer(Widget child) {
|
|
||||||
return Selector<AppState, num>(
|
|
||||||
selector: (_, appState) {
|
|
||||||
return appState.checkIpNum;
|
|
||||||
},
|
|
||||||
shouldRebuild: (prev, next) {
|
|
||||||
if (prev != next) {
|
|
||||||
_startCheck();
|
|
||||||
}
|
|
||||||
return prev != next;
|
|
||||||
},
|
|
||||||
builder: (_, checkIpNum, child) {
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_countryCodeToEmoji(String countryCode) {
|
_countryCodeToEmoji(String countryCode) {
|
||||||
final String code = countryCode.toUpperCase();
|
final String code = countryCode.toUpperCase();
|
||||||
if (code.length != 2) {
|
if (code.length != 2) {
|
||||||
@@ -141,109 +130,130 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: getWidgetHeight(1),
|
height: getWidgetHeight(1),
|
||||||
child: _checkIpContainer(
|
child: ValueListenableBuilder<NetworkDetectionState>(
|
||||||
ValueListenableBuilder<NetworkDetectionState>(
|
valueListenable: _networkDetectionState,
|
||||||
valueListenable: _networkDetectionState,
|
builder: (_, state, __) {
|
||||||
builder: (_, state, __) {
|
final ipInfo = state.ipInfo;
|
||||||
final ipInfo = state.ipInfo;
|
final isTesting = state.isTesting;
|
||||||
final isTesting = state.isTesting;
|
return CommonCard(
|
||||||
return CommonCard(
|
onPressed: () {},
|
||||||
onPressed: () {},
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
height: globalState.measure.titleMediumHeight + 16,
|
||||||
height: globalState.measure.titleMediumHeight + 16,
|
padding: baseInfoEdgeInsets.copyWith(
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
bottom: 0,
|
||||||
bottom: 0,
|
),
|
||||||
),
|
child: Row(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisSize: MainAxisSize.max,
|
children: [
|
||||||
children: [
|
ipInfo != null
|
||||||
ipInfo != null
|
? Text(
|
||||||
? Text(
|
_countryCodeToEmoji(
|
||||||
_countryCodeToEmoji(
|
ipInfo.countryCode,
|
||||||
ipInfo.countryCode,
|
|
||||||
),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleMedium
|
|
||||||
?.toLight
|
|
||||||
.copyWith(
|
|
||||||
fontFamily: FontFamily.twEmoji.value,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Icons.network_check,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TooltipText(
|
|
||||||
text: Text(
|
|
||||||
appLocalizations.networkDetection,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleSmall
|
.titleMedium
|
||||||
?.copyWith(
|
?.toLight
|
||||||
color: context.colorScheme.onSurfaceVariant,
|
.copyWith(
|
||||||
|
fontFamily: FontFamily.twEmoji.value,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.network_check,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TooltipText(
|
||||||
|
text: Text(
|
||||||
|
appLocalizations.networkDetection,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
SizedBox(width: 2),
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(
|
||||||
|
text: appLocalizations.detectionTip,
|
||||||
|
),
|
||||||
|
cancelable: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
size: 16,
|
||||||
|
Icons.info_outline,
|
||||||
|
color: context.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Container(
|
),
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
Container(
|
||||||
top: 0,
|
padding: baseInfoEdgeInsets.copyWith(
|
||||||
),
|
top: 0,
|
||||||
child: SizedBox(
|
),
|
||||||
height: globalState.measure.bodyMediumHeight + 2,
|
child: SizedBox(
|
||||||
child: FadeBox(
|
height: globalState.measure.bodyMediumHeight + 2,
|
||||||
child: ipInfo != null
|
child: FadeBox(
|
||||||
? TooltipText(
|
child: ipInfo != null
|
||||||
text: Text(
|
? TooltipText(
|
||||||
ipInfo.ip,
|
text: Text(
|
||||||
style: context.textTheme.bodyMedium?.toLight
|
ipInfo.ip,
|
||||||
.adjustSize(1),
|
style: context.textTheme.bodyMedium?.toLight
|
||||||
maxLines: 1,
|
.adjustSize(1),
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
),
|
||||||
: FadeBox(
|
)
|
||||||
child: isTesting == false && ipInfo == null
|
: FadeBox(
|
||||||
? Text(
|
child: isTesting == false && ipInfo == null
|
||||||
"timeout",
|
? Text(
|
||||||
style: context.textTheme.bodyMedium
|
"timeout",
|
||||||
?.copyWith(color: Colors.red)
|
style: context.textTheme.bodyMedium
|
||||||
.adjustSize(1),
|
?.copyWith(color: Colors.red)
|
||||||
maxLines: 1,
|
.adjustSize(1),
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
)
|
overflow: TextOverflow.ellipsis,
|
||||||
: Container(
|
)
|
||||||
padding: const EdgeInsets.all(2),
|
: Container(
|
||||||
child: const AspectRatio(
|
padding: const EdgeInsets.all(2),
|
||||||
aspectRatio: 1,
|
child: const AspectRatio(
|
||||||
child: CircularProgressIndicator(),
|
aspectRatio: 1,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class NetworkSpeed extends StatefulWidget {
|
class NetworkSpeed extends StatefulWidget {
|
||||||
const NetworkSpeed({super.key});
|
const NetworkSpeed({super.key});
|
||||||
@@ -49,9 +50,9 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
|||||||
label: appLocalizations.networkSpeed,
|
label: appLocalizations.networkSpeed,
|
||||||
iconData: Icons.speed_sharp,
|
iconData: Icons.speed_sharp,
|
||||||
),
|
),
|
||||||
child: Selector<AppFlowingState, List<Traffic>>(
|
child: Consumer(
|
||||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
builder: (_, ref, __) {
|
||||||
builder: (_, traffics, __) {
|
final traffics = ref.watch(trafficsProvider).list;
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -76,41 +77,11 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
|||||||
-16,
|
-16,
|
||||||
-20,
|
-20,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
"${_getLastTraffic(traffics).up}↑ ${_getLastTraffic(traffics).down}↓",
|
||||||
children: [
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
Icon(
|
color: color,
|
||||||
Icons.arrow_upward,
|
),
|
||||||
color: color,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${_getLastTraffic(traffics).up}/s",
|
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_downward,
|
|
||||||
color: color,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${_getLastTraffic(traffics).down}/s",
|
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class OutboundMode extends StatelessWidget {
|
class OutboundMode extends StatelessWidget {
|
||||||
const OutboundMode({super.key});
|
const OutboundMode({super.key});
|
||||||
@@ -15,9 +15,10 @@ class OutboundMode extends StatelessWidget {
|
|||||||
final height = getWidgetHeight(2);
|
final height = getWidgetHeight(2);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: height,
|
height: height,
|
||||||
child: Selector<ClashConfig, Mode>(
|
child: Consumer(
|
||||||
selector: (_, clashConfig) => clashConfig.mode,
|
builder: (_, ref, __) {
|
||||||
builder: (_, mode, __) {
|
final mode =
|
||||||
|
ref.watch(patchClashConfigProvider.select((state) => state.mode));
|
||||||
return CommonCard(
|
return CommonCard(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
info: Info(
|
info: Info(
|
||||||
|
|||||||
@@ -1,78 +1,76 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/fragments/config/network.dart';
|
import 'package:fl_clash/fragments/config/network.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class TUNButton extends StatelessWidget {
|
class TUNButton extends StatelessWidget {
|
||||||
const TUNButton({super.key});
|
const TUNButton({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LocaleBuilder(
|
return SizedBox(
|
||||||
builder: (_) => SizedBox(
|
height: getWidgetHeight(1),
|
||||||
height: getWidgetHeight(1),
|
child: CommonCard(
|
||||||
child: CommonCard(
|
onPressed: () {
|
||||||
onPressed: () {
|
showSheet(
|
||||||
showSheet(
|
context: context,
|
||||||
context: context,
|
body: generateListView(generateSection(
|
||||||
body: generateListView(generateSection(
|
items: [
|
||||||
items: [
|
if (system.isDesktop) const TUNItem(),
|
||||||
if (system.isDesktop) const TUNItem(),
|
const TunStackItem(),
|
||||||
const TunStackItem(),
|
],
|
||||||
],
|
)),
|
||||||
)),
|
title: appLocalizations.tun,
|
||||||
title: appLocalizations.tun,
|
);
|
||||||
);
|
},
|
||||||
},
|
info: Info(
|
||||||
info: Info(
|
label: appLocalizations.tun,
|
||||||
label: appLocalizations.tun,
|
iconData: Icons.stacked_line_chart,
|
||||||
iconData: Icons.stacked_line_chart,
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: baseInfoEdgeInsets.copyWith(
|
||||||
|
top: 4,
|
||||||
|
bottom: 8,
|
||||||
|
right: 8,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Row(
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
mainAxisSize: MainAxisSize.max,
|
||||||
top: 4,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
bottom: 8,
|
children: [
|
||||||
right: 8,
|
Flexible(
|
||||||
),
|
flex: 1,
|
||||||
child: Row(
|
child: TooltipText(
|
||||||
mainAxisSize: MainAxisSize.max,
|
text: Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
appLocalizations.options,
|
||||||
children: [
|
maxLines: 1,
|
||||||
Flexible(
|
overflow: TextOverflow.ellipsis,
|
||||||
flex: 1,
|
style: Theme.of(context)
|
||||||
child: TooltipText(
|
.textTheme
|
||||||
text: Text(
|
.titleSmall
|
||||||
appLocalizations.options,
|
?.adjustSize(-2)
|
||||||
maxLines: 1,
|
.toLight,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleSmall
|
|
||||||
?.adjustSize(-2)
|
|
||||||
.toLight,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Selector<ClashConfig, bool>(
|
),
|
||||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
Consumer(
|
||||||
builder: (_, enable, __) {
|
builder: (_, ref, __) {
|
||||||
return Switch(
|
final enable = ref.watch(patchClashConfigProvider
|
||||||
value: enable,
|
.select((state) => state.tun.enable));
|
||||||
onChanged: (value) {
|
return Switch(
|
||||||
final clashConfig =
|
value: enable,
|
||||||
globalState.appController.clashConfig;
|
onChanged: (value) {
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
enable: value,
|
(state) => state.copyWith.tun(
|
||||||
);
|
enable: value,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
],
|
},
|
||||||
),
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -87,67 +85,68 @@ class SystemProxyButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: getWidgetHeight(1),
|
height: getWidgetHeight(1),
|
||||||
child: LocaleBuilder(
|
child: CommonCard(
|
||||||
builder: (_) => CommonCard(
|
onPressed: () {
|
||||||
onPressed: () {
|
showSheet(
|
||||||
showSheet(
|
context: context,
|
||||||
context: context,
|
body: generateListView(
|
||||||
body: generateListView(
|
generateSection(
|
||||||
generateSection(
|
items: [
|
||||||
items: [
|
SystemProxyItem(),
|
||||||
SystemProxyItem(),
|
BypassDomainItem(),
|
||||||
BypassDomainItem(),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
title: appLocalizations.systemProxy,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.systemProxy,
|
|
||||||
iconData: Icons.shuffle,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
top: 4,
|
|
||||||
bottom: 8,
|
|
||||||
right: 8,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
title: appLocalizations.systemProxy,
|
||||||
mainAxisSize: MainAxisSize.max,
|
);
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
},
|
||||||
children: [
|
info: Info(
|
||||||
Flexible(
|
label: appLocalizations.systemProxy,
|
||||||
flex: 1,
|
iconData: Icons.shuffle,
|
||||||
child: TooltipText(
|
),
|
||||||
text: Text(
|
child: Container(
|
||||||
appLocalizations.options,
|
padding: baseInfoEdgeInsets.copyWith(
|
||||||
maxLines: 1,
|
top: 4,
|
||||||
overflow: TextOverflow.ellipsis,
|
bottom: 8,
|
||||||
style: Theme.of(context)
|
right: 8,
|
||||||
.textTheme
|
),
|
||||||
.titleSmall
|
child: Row(
|
||||||
?.adjustSize(-2)
|
mainAxisSize: MainAxisSize.max,
|
||||||
.toLight,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
),
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TooltipText(
|
||||||
|
text: Text(
|
||||||
|
appLocalizations.options,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall
|
||||||
|
?.adjustSize(-2)
|
||||||
|
.toLight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Selector<Config, bool>(
|
),
|
||||||
selector: (_, config) => config.networkProps.systemProxy,
|
Consumer(
|
||||||
builder: (_, systemProxy, __) {
|
builder: (_, ref, __) {
|
||||||
return Switch(
|
final systemProxy = ref.watch(networkSettingProvider
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
.select((state) => state.systemProxy));
|
||||||
value: systemProxy,
|
return Switch(
|
||||||
onChanged: (value) {
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
final config = globalState.appController.config;
|
value: systemProxy,
|
||||||
config.networkProps =
|
onChanged: (value) {
|
||||||
config.networkProps.copyWith(systemProxy: value);
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
},
|
(state) => state.copyWith(
|
||||||
);
|
systemProxy: value,
|
||||||
},
|
),
|
||||||
)
|
);
|
||||||
],
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class StartButton extends StatefulWidget {
|
class StartButton extends StatefulWidget {
|
||||||
const StartButton({super.key});
|
const StartButton({super.key});
|
||||||
@@ -19,7 +20,7 @@ class _StartButtonState extends State<StartButton>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
isStart = globalState.appController.appFlowingState.isStart;
|
isStart = globalState.appState.runTime != null;
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
value: isStart ? 1 : 0,
|
value: isStart ? 1 : 0,
|
||||||
@@ -34,11 +35,10 @@ class _StartButtonState extends State<StartButton>
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchStart() {
|
handleSwitchStart() {
|
||||||
final appController = globalState.appController;
|
if (isStart == globalState.appState.isStart) {
|
||||||
if (isStart == appController.appFlowingState.isStart) {
|
|
||||||
isStart = !isStart;
|
isStart = !isStart;
|
||||||
updateController();
|
updateController();
|
||||||
appController.updateStatus(isStart);
|
globalState.appController.updateStatus(isStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,31 +50,24 @@ class _StartButtonState extends State<StartButton>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _updateControllerContainer(Widget child) {
|
|
||||||
return Selector<AppFlowingState, bool>(
|
|
||||||
selector: (_, appFlowingState) => appFlowingState.isStart,
|
|
||||||
builder: (_, isStart, child) {
|
|
||||||
if (isStart != this.isStart) {
|
|
||||||
this.isStart = isStart;
|
|
||||||
updateController();
|
|
||||||
}
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector2<AppState, Config, StartButtonSelectorState>(
|
return Consumer(
|
||||||
selector: (_, appState, config) => StartButtonSelectorState(
|
builder: (_, ref, child) {
|
||||||
isInit: appState.isInit,
|
final state = ref.watch(startButtonSelectorStateProvider);
|
||||||
hasProfile: config.profiles.isNotEmpty,
|
|
||||||
),
|
|
||||||
builder: (_, state, child) {
|
|
||||||
if (!state.isInit || !state.hasProfile) {
|
if (!state.isInit || !state.hasProfile) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
ref.listenManual(
|
||||||
|
runTimeProvider.select((state) => state != null),
|
||||||
|
(prev, next) {
|
||||||
|
if (next != isStart) {
|
||||||
|
isStart = next;
|
||||||
|
updateController();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
final textWidth = globalState.measure
|
final textWidth = globalState.measure
|
||||||
.computeTextSize(
|
.computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
@@ -86,53 +79,51 @@ class _StartButtonState extends State<StartButton>
|
|||||||
)
|
)
|
||||||
.width +
|
.width +
|
||||||
16;
|
16;
|
||||||
return _updateControllerContainer(
|
return AnimatedBuilder(
|
||||||
AnimatedBuilder(
|
animation: _controller.view,
|
||||||
animation: _controller.view,
|
builder: (_, child) {
|
||||||
builder: (_, child) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: 56 + textWidth * _controller.value,
|
||||||
width: 56 + textWidth * _controller.value,
|
height: 56,
|
||||||
height: 56,
|
child: FloatingActionButton(
|
||||||
child: FloatingActionButton(
|
heroTag: null,
|
||||||
heroTag: null,
|
onPressed: () {
|
||||||
onPressed: () {
|
handleSwitchStart();
|
||||||
handleSwitchStart();
|
},
|
||||||
},
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
width: 56,
|
||||||
width: 56,
|
height: 56,
|
||||||
height: 56,
|
alignment: Alignment.center,
|
||||||
alignment: Alignment.center,
|
child: AnimatedIcon(
|
||||||
child: AnimatedIcon(
|
icon: AnimatedIcons.play_pause,
|
||||||
icon: AnimatedIcons.play_pause,
|
progress: _controller,
|
||||||
progress: _controller,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: ClipRect(
|
Expanded(
|
||||||
child: OverflowBox(
|
child: ClipRect(
|
||||||
maxWidth: textWidth,
|
child: OverflowBox(
|
||||||
child: Container(
|
maxWidth: textWidth,
|
||||||
alignment: Alignment.centerLeft,
|
child: Container(
|
||||||
child: child!,
|
alignment: Alignment.centerLeft,
|
||||||
),
|
child: child!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
child: child,
|
},
|
||||||
),
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Selector<AppFlowingState, int?>(
|
child: Consumer(
|
||||||
selector: (_, appFlowingState) => appFlowingState.runTime,
|
builder: (_, ref, __) {
|
||||||
builder: (_, int? value, __) {
|
final runTime = ref.watch(runTimeProvider);
|
||||||
final text = other.getTimeText(value);
|
final text = other.getTimeText(runTime);
|
||||||
return Text(
|
return Text(
|
||||||
text,
|
text,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class TrafficUsage extends StatelessWidget {
|
class TrafficUsage extends StatelessWidget {
|
||||||
const TrafficUsage({super.key});
|
const TrafficUsage({super.key});
|
||||||
@@ -62,9 +63,9 @@ class TrafficUsage extends StatelessWidget {
|
|||||||
iconData: Icons.data_saver_off,
|
iconData: Icons.data_saver_off,
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
child: Selector<AppFlowingState, Traffic>(
|
child: Consumer(
|
||||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
builder: (_, ref, __) {
|
||||||
builder: (_, totalTraffic, __) {
|
final totalTraffic = ref.watch(totalTrafficProvider);
|
||||||
final upTotalTrafficValue = totalTraffic.up;
|
final upTotalTrafficValue = totalTraffic.up;
|
||||||
final downTotalTrafficValue = totalTraffic.down;
|
final downTotalTrafficValue = totalTraffic.down;
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
|
|||||||
export 'tools.dart';
|
export 'tools.dart';
|
||||||
export 'profiles/profiles.dart';
|
export 'profiles/profiles.dart';
|
||||||
export 'logs.dart';
|
export 'logs.dart';
|
||||||
export 'connections.dart';
|
|
||||||
export 'access.dart';
|
export 'access.dart';
|
||||||
export 'config/config.dart';
|
export 'config/config.dart';
|
||||||
export 'application_setting.dart';
|
export 'application_setting.dart';
|
||||||
export 'about.dart';
|
export 'about.dart';
|
||||||
export 'backup_and_recovery.dart';
|
export 'backup_and_recovery.dart';
|
||||||
export 'resources.dart';
|
export 'resources.dart';
|
||||||
export 'requests.dart';
|
export 'connection/requests.dart';
|
||||||
|
export 'connection/connections.dart';
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/card.dart';
|
import 'package:fl_clash/widgets/card.dart';
|
||||||
import 'package:fl_clash/widgets/list.dart';
|
import 'package:fl_clash/widgets/list.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
extension IntlExt on Intl {
|
extension IntlExt on Intl {
|
||||||
static actionMessage(String messageText) =>
|
static actionMessage(String messageText) =>
|
||||||
@@ -38,29 +39,20 @@ class HotKeyFragment extends StatelessWidget {
|
|||||||
itemCount: HotAction.values.length,
|
itemCount: HotAction.values.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final hotAction = HotAction.values[index];
|
final hotAction = HotAction.values[index];
|
||||||
return Selector<Config, HotKeyAction>(
|
return Consumer(
|
||||||
selector: (_, config) {
|
builder: (_, ref, __) {
|
||||||
final index = config.hotKeyActions.indexWhere(
|
final hotKeyAction = ref.watch(getHotKeyActionProvider(hotAction));
|
||||||
(item) => item.action == hotAction,
|
|
||||||
);
|
|
||||||
return index != -1
|
|
||||||
? config.hotKeyActions[index]
|
|
||||||
: HotKeyAction(
|
|
||||||
action: hotAction,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
builder: (_, value, __) {
|
|
||||||
return ListItem(
|
return ListItem(
|
||||||
title: Text(IntlExt.actionMessage(hotAction.name)),
|
title: Text(IntlExt.actionMessage(hotAction.name)),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
getSubtitle(value),
|
getSubtitle(hotKeyAction),
|
||||||
style: context.textTheme.bodyMedium
|
style: context.textTheme.bodyMedium
|
||||||
?.copyWith(color: context.colorScheme.primary),
|
?.copyWith(color: context.colorScheme.primary),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
globalState.showCommonDialog(
|
globalState.showCommonDialog(
|
||||||
child: HotKeyRecorder(
|
child: HotKeyRecorder(
|
||||||
hotKeyAction: value,
|
hotKeyAction: hotKeyAction,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -121,8 +113,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
|||||||
|
|
||||||
_handleRemove() {
|
_handleRemove() {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
final config = globalState.appController.config;
|
globalState.appController.updateOrAddHotKeyAction(
|
||||||
config.updateOrAddHotKeyAction(
|
|
||||||
hotKeyActionNotifier.value.copyWith(
|
hotKeyActionNotifier.value.copyWith(
|
||||||
modifiers: {},
|
modifiers: {},
|
||||||
key: null,
|
key: null,
|
||||||
@@ -132,7 +123,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
|||||||
|
|
||||||
_handleConfirm() {
|
_handleConfirm() {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
final config = globalState.appController.config;
|
final config = globalState.config;
|
||||||
final currentHotkeyAction = hotKeyActionNotifier.value;
|
final currentHotkeyAction = hotKeyActionNotifier.value;
|
||||||
if (currentHotkeyAction.key == null ||
|
if (currentHotkeyAction.key == null ||
|
||||||
currentHotkeyAction.modifiers.isEmpty) {
|
currentHotkeyAction.modifiers.isEmpty) {
|
||||||
@@ -158,7 +149,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
config.updateOrAddHotKeyAction(
|
globalState.appController.updateOrAddHotKeyAction(
|
||||||
currentHotkeyAction,
|
currentHotkeyAction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,106 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
|
|
||||||
class LogsFragment extends StatefulWidget {
|
double _preOffset = 0;
|
||||||
|
|
||||||
|
class LogsFragment extends ConsumerStatefulWidget {
|
||||||
const LogsFragment({super.key});
|
const LogsFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LogsFragment> createState() => _LogsFragmentState();
|
ConsumerState<LogsFragment> createState() => _LogsFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LogsFragmentState extends State<LogsFragment> {
|
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||||
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords());
|
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
|
||||||
final scrollController = ScrollController(
|
final _scrollController = ScrollController(
|
||||||
keepScrollOffset: false,
|
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||||
);
|
);
|
||||||
|
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||||
|
double _currentMaxWidth = 0;
|
||||||
|
|
||||||
Timer? timer;
|
List<Log> _logs = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||||
final appFlowingState = globalState.appController.appFlowingState;
|
logs: globalState.appState.logs.list,
|
||||||
logsNotifier.value =
|
);
|
||||||
logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
ref.listenManual(
|
||||||
if (timer != null) {
|
logsProvider.select((state) => state.list),
|
||||||
timer?.cancel();
|
(prev, next) {
|
||||||
timer = null;
|
if (prev != next) {
|
||||||
}
|
final isEquality = logListEquality.equals(prev, next);
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
if (!isEquality) {
|
||||||
final logs = appFlowingState.logs;
|
_logs = next;
|
||||||
if (!logListEquality.equals(
|
updateLogsThrottler();
|
||||||
logsNotifier.value.logs,
|
}
|
||||||
logs,
|
|
||||||
)) {
|
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
|
||||||
logs: logs,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
});
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
ref.listenManual(
|
||||||
|
isCurrentPageProvider(
|
||||||
|
PageLabel.logs,
|
||||||
|
handler: (pageLabel, viewMode) =>
|
||||||
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||||
|
),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next && next == true) {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_handleExport();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.file_download_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_logsStateNotifier.value =
|
||||||
|
_logsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
timer?.cancel();
|
_logsStateNotifier.dispose();
|
||||||
logsNotifier.dispose();
|
_scrollController.dispose();
|
||||||
scrollController.dispose();
|
_cacheDynamicHeightMap.clear();
|
||||||
timer = null;
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleTryClearCache(double maxWidth) {
|
||||||
|
if (_currentMaxWidth != maxWidth) {
|
||||||
|
_currentMaxWidth = maxWidth;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_handleExport() async {
|
_handleExport() async {
|
||||||
final commonScaffoldState = context.commonScaffoldState;
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||||
@@ -71,295 +116,115 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initActions() {
|
double _calcCacheHeight(String text) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
final cacheHeight = _cacheDynamicHeightMap.get(text);
|
||||||
final commonScaffoldState =
|
if (cacheHeight != null) {
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
return cacheHeight;
|
||||||
commonScaffoldState?.actions = [
|
}
|
||||||
IconButton(
|
final size = globalState.measure.computeTextSize(
|
||||||
onPressed: () {
|
Text(
|
||||||
showSearch(
|
text,
|
||||||
context: context,
|
style: globalState.appController.context.textTheme.bodyLarge,
|
||||||
delegate: LogsSearchDelegate(
|
),
|
||||||
logs: logsNotifier.value,
|
maxWidth: _currentMaxWidth,
|
||||||
),
|
);
|
||||||
);
|
_cacheDynamicHeightMap.put(text, size.height);
|
||||||
},
|
return size.height;
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
_handleExport();
|
|
||||||
},
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.file_download_outlined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
double _getItemHeight(Log log) {
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
final measure = globalState.measure;
|
||||||
if (isContains) return;
|
final bodySmallHeight = measure.bodySmallHeight;
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||||
..add(keyword);
|
final height = _calcCacheHeight(log.payload ?? "");
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
updateLogsThrottler() {
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
throttler.call("logs", () {
|
||||||
if (!isContains) return;
|
final isEquality = logListEquality.equals(
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
_logs,
|
||||||
..remove(keyword);
|
_logsStateNotifier.value.logs,
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
);
|
||||||
keywords: keywords,
|
if (isEquality) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||||
|
logs: _logs,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, duration: commonDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<AppState, bool?>(
|
return LayoutBuilder(
|
||||||
selector: (_, appState) =>
|
builder: (_, constraints) {
|
||||||
appState.currentLabel == 'logs' ||
|
_handleTryClearCache(constraints.maxWidth - 40);
|
||||||
appState.viewMode == ViewMode.mobile &&
|
return Align(
|
||||||
appState.currentLabel == "tools",
|
alignment: Alignment.topCenter,
|
||||||
builder: (_, isCurrent, child) {
|
child: ValueListenableBuilder<LogsState>(
|
||||||
if (isCurrent == null || isCurrent) {
|
valueListenable: _logsStateNotifier,
|
||||||
_initActions();
|
builder: (_, state, __) {
|
||||||
}
|
final logs = state.list;
|
||||||
return child!;
|
if (logs.isEmpty) {
|
||||||
},
|
return NullStatus(
|
||||||
child: ValueListenableBuilder<LogsAndKeywords>(
|
label: appLocalizations.nullLogsDesc,
|
||||||
valueListenable: logsNotifier,
|
);
|
||||||
builder: (_, state, __) {
|
}
|
||||||
final logs = state.filteredLogs;
|
final items = logs
|
||||||
if (logs.isEmpty) {
|
.map<Widget>(
|
||||||
return NullStatus(
|
(log) => LogItem(
|
||||||
label: appLocalizations.nullLogsDesc,
|
key: Key(log.dateTime.toString()),
|
||||||
);
|
log: log,
|
||||||
}
|
onClick: (value) {
|
||||||
final reversedLogs = logs.reversed.toList();
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
final logWidgets = reversedLogs
|
},
|
||||||
.map<Widget>(
|
),
|
||||||
(log) => LogItem(
|
)
|
||||||
key: Key(log.dateTime.toString()),
|
.separated(
|
||||||
log: log,
|
const Divider(
|
||||||
onClick: _addKeyword,
|
height: 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.separated(
|
.toList();
|
||||||
const Divider(
|
return NotificationListener<ScrollEndNotification>(
|
||||||
height: 0,
|
onNotification: (details) {
|
||||||
),
|
_preOffset = details.metrics.pixels;
|
||||||
)
|
return false;
|
||||||
.toList();
|
},
|
||||||
return Column(
|
child: CommonScrollBar(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
controller: _scrollController,
|
||||||
children: [
|
child: ListView.builder(
|
||||||
if (state.keywords.isNotEmpty)
|
reverse: true,
|
||||||
Padding(
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.symmetric(
|
physics: NextClampingScrollPhysics(),
|
||||||
horizontal: 16,
|
controller: _scrollController,
|
||||||
vertical: 16,
|
itemBuilder: (_, index) {
|
||||||
),
|
return items[index];
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (_, constraints) {
|
|
||||||
return ScrollConfiguration(
|
|
||||||
behavior: ShowBarScrollBehavior(),
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
itemExtentBuilder: (index, __) {
|
|
||||||
final widget = logWidgets[index];
|
|
||||||
if (widget.runtimeType == Divider) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
final measure = globalState.measure;
|
|
||||||
final bodyLargeSize = measure.bodyLargeSize;
|
|
||||||
final bodySmallHeight = measure.bodySmallHeight;
|
|
||||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
|
||||||
final log = reversedLogs[(index / 2).floor()];
|
|
||||||
final width = (log.payload?.length ?? 0) *
|
|
||||||
bodyLargeSize.width +
|
|
||||||
200;
|
|
||||||
final lines = (width / constraints.maxWidth).ceil();
|
|
||||||
return lines * bodyLargeSize.height +
|
|
||||||
bodySmallHeight +
|
|
||||||
8 +
|
|
||||||
bodyMediumHeight +
|
|
||||||
40;
|
|
||||||
},
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
return logWidgets[index];
|
|
||||||
},
|
|
||||||
itemCount: logWidgets.length,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogsSearchDelegate extends SearchDelegate {
|
|
||||||
ValueNotifier<LogsAndKeywords> logsNotifier;
|
|
||||||
|
|
||||||
LogsSearchDelegate({
|
|
||||||
required LogsAndKeywords logs,
|
|
||||||
}) : logsNotifier = ValueNotifier(logs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
logsNotifier.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
get state => logsNotifier.value;
|
|
||||||
|
|
||||||
List<Log> get _results {
|
|
||||||
final lowQuery = query.toLowerCase();
|
|
||||||
return logsNotifier.value.filteredLogs
|
|
||||||
.where(
|
|
||||||
(log) =>
|
|
||||||
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
|
|
||||||
log.logLevel.name.contains(lowQuery),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget>? buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
close(context, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query = '';
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close(context, null);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildResults(BuildContext context) {
|
|
||||||
return buildSuggestions(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: logsNotifier,
|
|
||||||
builder: (_, __, ___) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final log = _results[index];
|
|
||||||
return LogItem(
|
|
||||||
key: Key(log.dateTime.toString()),
|
|
||||||
log: log,
|
|
||||||
onClick: (value) {
|
|
||||||
_addKeyword(value);
|
|
||||||
},
|
},
|
||||||
);
|
itemExtentBuilder: (index, __) {
|
||||||
},
|
final item = items[index];
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
if (item.runtimeType == Divider) {
|
||||||
return const Divider(
|
return 0;
|
||||||
height: 0,
|
}
|
||||||
);
|
final log = logs[(index / 2).floor()];
|
||||||
},
|
return _getItemHeight(log);
|
||||||
itemCount: _results.length,
|
},
|
||||||
),
|
itemCount: items.length,
|
||||||
)
|
),
|
||||||
],
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogItem extends StatefulWidget {
|
class LogItem extends StatelessWidget {
|
||||||
final Log log;
|
final Log log;
|
||||||
final Function(String)? onClick;
|
final Function(String)? onClick;
|
||||||
|
|
||||||
@@ -369,14 +234,8 @@ class LogItem extends StatefulWidget {
|
|||||||
this.onClick,
|
this.onClick,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<LogItem> createState() => _LogItemState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogItemState extends State<LogItem> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final log = widget.log;
|
|
||||||
return ListItem(
|
return ListItem(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
@@ -384,14 +243,16 @@ class _LogItemState extends State<LogItem> {
|
|||||||
),
|
),
|
||||||
title: SelectableText(
|
title: SelectableText(
|
||||||
log.payload ?? '',
|
log.payload ?? '',
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SelectableText(
|
SelectableText(
|
||||||
"${log.dateTime}",
|
"${log.dateTime}",
|
||||||
style: context.textTheme.bodySmall
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
?.copyWith(color: context.colorScheme.primary),
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
@@ -400,8 +261,8 @@ class _LogItemState extends State<LogItem> {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: CommonChip(
|
child: CommonChip(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (widget.onClick == null) return;
|
if (onClick == null) return;
|
||||||
widget.onClick!(log.logLevel.name);
|
onClick!(log.logLevel.name);
|
||||||
},
|
},
|
||||||
label: log.logLevel.name,
|
label: log.logLevel.name,
|
||||||
),
|
),
|
||||||
@@ -411,3 +272,11 @@ class _LogItemState extends State<LogItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NoGlowScrollBehavior extends ScrollBehavior {
|
||||||
|
@override
|
||||||
|
Widget buildOverscrollIndicator(
|
||||||
|
BuildContext context, Widget child, ScrollableDetails details) {
|
||||||
|
return child; // 禁用过度滚动效果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
54
lib/fragments/profiles/custom_profile.dart
Normal file
54
lib/fragments/profiles/custom_profile.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/widgets/scaffold.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomProfile extends StatefulWidget {
|
||||||
|
final String profileId;
|
||||||
|
|
||||||
|
const CustomProfile({
|
||||||
|
super.key,
|
||||||
|
required this.profileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomProfile> createState() => _CustomProfileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomProfileState extends State<CustomProfile> {
|
||||||
|
final _currentClashConfigNotifier = ValueNotifier<ClashConfig?>(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initCurrentClashConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initCurrentClashConfig() async {
|
||||||
|
// final currentProfileId = globalState.config.currentProfileId;
|
||||||
|
// if (currentProfileId == null) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// _currentClashConfigNotifier.value =
|
||||||
|
// await clashCore.getProfile(currentProfileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonScaffold(
|
||||||
|
body: ValueListenableBuilder(
|
||||||
|
valueListenable: _currentClashConfigNotifier,
|
||||||
|
builder: (_, clashConfig, ___) {
|
||||||
|
if (clashConfig == null) {
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: "自定义",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/pages/editor.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class EditProfile extends StatefulWidget {
|
class EditProfile extends StatefulWidget {
|
||||||
final Profile profile;
|
final Profile profile;
|
||||||
@@ -30,10 +30,13 @@ class _EditProfileState extends State<EditProfile> {
|
|||||||
late TextEditingController urlController;
|
late TextEditingController urlController;
|
||||||
late TextEditingController autoUpdateDurationController;
|
late TextEditingController autoUpdateDurationController;
|
||||||
late bool autoUpdate;
|
late bool autoUpdate;
|
||||||
|
String? rawText;
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||||
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
final fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||||
Uint8List? fileData;
|
Uint8List? fileData;
|
||||||
|
|
||||||
|
Profile get profile => widget.profile;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -51,28 +54,43 @@ class _EditProfileState extends State<EditProfile> {
|
|||||||
|
|
||||||
_handleConfirm() async {
|
_handleConfirm() async {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
final config = widget.context.read<Config>();
|
final appController = globalState.appController;
|
||||||
final profile = widget.profile.copyWith(
|
Profile profile = this.profile.copyWith(
|
||||||
url: urlController.text,
|
url: urlController.text,
|
||||||
label: labelController.text,
|
label: labelController.text,
|
||||||
autoUpdate: autoUpdate,
|
autoUpdate: autoUpdate,
|
||||||
autoUpdateDuration: Duration(
|
autoUpdateDuration: Duration(
|
||||||
minutes: int.parse(
|
minutes: int.parse(
|
||||||
autoUpdateDurationController.text,
|
autoUpdateDurationController.text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final hasUpdate = widget.profile.url != profile.url;
|
final hasUpdate = widget.profile.url != profile.url;
|
||||||
if (fileData != null) {
|
if (fileData != null) {
|
||||||
config.setProfile(await profile.saveFile(fileData!));
|
if (profile.type == ProfileType.url && autoUpdate) {
|
||||||
|
final res = await globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(
|
||||||
|
text: appLocalizations.profileHasUpdate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (res == true) {
|
||||||
|
profile = profile.copyWith(
|
||||||
|
autoUpdate: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appController.setProfile(await profile.saveFile(fileData!));
|
||||||
|
} else if (!hasUpdate) {
|
||||||
|
appController.setProfile(profile);
|
||||||
} else {
|
} else {
|
||||||
config.setProfile(profile);
|
|
||||||
}
|
|
||||||
if (hasUpdate) {
|
|
||||||
globalState.homeScaffoldKey.currentState?.loadingRun(
|
globalState.homeScaffoldKey.currentState?.loadingRun(
|
||||||
() async {
|
() async {
|
||||||
|
await Future.delayed(
|
||||||
|
commonDuration,
|
||||||
|
);
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
await globalState.appController.updateProfile(profile);
|
await appController.updateProfile(profile);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -102,22 +120,69 @@ class _EditProfileState extends State<EditProfile> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_editProfileFile() async {
|
_handleSaveEdit(BuildContext context, String data) async {
|
||||||
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
final message = await globalState.safeRun<String>(
|
||||||
if (profilePath == null) return;
|
() async {
|
||||||
globalState.safeRun(() async {
|
final message = await clashCore.validateConfig(data);
|
||||||
if (Platform.isAndroid) {
|
return message;
|
||||||
await app?.openFile(
|
},
|
||||||
profilePath,
|
silence: false,
|
||||||
);
|
);
|
||||||
return;
|
if (message?.isNotEmpty == true) {
|
||||||
}
|
globalState.showMessage(
|
||||||
await launchUrl(
|
title: appLocalizations.tip,
|
||||||
Uri.file(
|
message: TextSpan(text: message),
|
||||||
profilePath,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_editProfileFile() async {
|
||||||
|
if (rawText == null) {
|
||||||
|
final profilePath = await appPath.getProfilePath(widget.profile.id);
|
||||||
|
if (profilePath == null) return;
|
||||||
|
final file = File(profilePath);
|
||||||
|
rawText = await file.readAsString();
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
final title = widget.profile.label ?? widget.profile.id;
|
||||||
|
final data = await BaseNavigator.push<String>(
|
||||||
|
globalState.homeScaffoldKey.currentContext!,
|
||||||
|
EditorPage(
|
||||||
|
title: title,
|
||||||
|
content: rawText!,
|
||||||
|
onSave: _handleSaveEdit,
|
||||||
|
onPop: (context, data) async {
|
||||||
|
if (data == rawText) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final res = await globalState.showMessage(
|
||||||
|
title: title,
|
||||||
|
message: TextSpan(
|
||||||
|
text: appLocalizations.hasCacheChange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (res == true && context.mounted) {
|
||||||
|
_handleSaveEdit(context, data);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rawText = data;
|
||||||
|
fileData = Uint8List.fromList(utf8.encode(data));
|
||||||
|
fileInfoNotifier.value = fileInfoNotifier.value?.copyWith(
|
||||||
|
size: fileData?.length ?? 0,
|
||||||
|
lastModified: DateTime.now(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_uploadProfileFile() async {
|
_uploadProfileFile() async {
|
||||||
@@ -130,6 +195,20 @@ class _EditProfileState extends State<EditProfile> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleBack() async {
|
||||||
|
final res = await globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(text: appLocalizations.fileIsUpdate),
|
||||||
|
);
|
||||||
|
if (res == true) {
|
||||||
|
_handleConfirm();
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final items = [
|
final items = [
|
||||||
@@ -245,34 +324,45 @@ class _EditProfileState extends State<EditProfile> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
return FloatLayout(
|
return PopScope(
|
||||||
floatingWidget: FloatWrapper(
|
canPop: false,
|
||||||
child: FloatingActionButton.extended(
|
onPopInvokedWithResult: (didPop, __) {
|
||||||
heroTag: null,
|
if (didPop) return;
|
||||||
onPressed: _handleConfirm,
|
if (fileData == null) {
|
||||||
label: Text(appLocalizations.save),
|
Navigator.of(context).pop();
|
||||||
icon: const Icon(Icons.save),
|
return;
|
||||||
),
|
}
|
||||||
),
|
_handleBack();
|
||||||
child: Form(
|
},
|
||||||
key: _formKey,
|
child: FloatLayout(
|
||||||
child: Padding(
|
floatingWidget: FloatWrapper(
|
||||||
padding: const EdgeInsets.symmetric(
|
child: FloatingActionButton.extended(
|
||||||
vertical: 16,
|
heroTag: null,
|
||||||
|
onPressed: _handleConfirm,
|
||||||
|
label: Text(appLocalizations.save),
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
),
|
),
|
||||||
child: ListView.separated(
|
),
|
||||||
padding: kMaterialListPadding.copyWith(
|
child: Form(
|
||||||
bottom: 72,
|
key: _formKey,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: kMaterialListPadding.copyWith(
|
||||||
|
bottom: 72,
|
||||||
|
),
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return items[index];
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: items.length,
|
||||||
),
|
),
|
||||||
itemBuilder: (_, index) {
|
|
||||||
return items[index];
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) {
|
|
||||||
return const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: items.length,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,21 +4,14 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
|
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'add_profile.dart';
|
import 'add_profile.dart';
|
||||||
|
|
||||||
enum PopupMenuItemEnum { delete, edit }
|
|
||||||
|
|
||||||
enum ProfileActions {
|
|
||||||
edit,
|
|
||||||
update,
|
|
||||||
delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProfilesFragment extends StatefulWidget {
|
class ProfilesFragment extends StatefulWidget {
|
||||||
const ProfilesFragment({super.key});
|
const ProfilesFragment({super.key});
|
||||||
|
|
||||||
@@ -26,7 +19,7 @@ class ProfilesFragment extends StatefulWidget {
|
|||||||
State<ProfilesFragment> createState() => _ProfilesFragmentState();
|
State<ProfilesFragment> createState() => _ProfilesFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProfilesFragmentState extends State<ProfilesFragment> {
|
class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
|
||||||
Function? applyConfigDebounce;
|
Function? applyConfigDebounce;
|
||||||
|
|
||||||
_handleShowAddExtendPage() {
|
_handleShowAddExtendPage() {
|
||||||
@@ -40,21 +33,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateProfiles() async {
|
_updateProfiles() async {
|
||||||
final appController = globalState.appController;
|
final profiles = globalState.config.profiles;
|
||||||
final config = appController.config;
|
|
||||||
final profiles = appController.config.profiles;
|
|
||||||
final messages = [];
|
final messages = [];
|
||||||
final updateProfiles = profiles.map<Future>(
|
final updateProfiles = profiles.map<Future>(
|
||||||
(profile) async {
|
(profile) async {
|
||||||
if (profile.type == ProfileType.file) return;
|
if (profile.type == ProfileType.file) return;
|
||||||
config.setProfile(
|
globalState.appController.setProfile(
|
||||||
profile.copyWith(isUpdating: true),
|
profile.copyWith(isUpdating: true),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await appController.updateProfile(profile);
|
await globalState.appController.updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
messages.add("${profile.label ?? profile.id}: $e \n");
|
messages.add("${profile.label ?? profile.id}: $e \n");
|
||||||
config.setProfile(
|
globalState.appController.setProfile(
|
||||||
profile.copyWith(
|
profile.copyWith(
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
),
|
),
|
||||||
@@ -77,97 +68,90 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initScaffold() {
|
@override
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
List<Widget> get actions => [
|
||||||
(_) {
|
IconButton(
|
||||||
if (!mounted) return;
|
onPressed: () {
|
||||||
final commonScaffoldState =
|
_updateProfiles();
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
},
|
||||||
commonScaffoldState?.actions = [
|
icon: const Icon(Icons.sync),
|
||||||
IconButton(
|
),
|
||||||
onPressed: () {
|
IconButton(
|
||||||
_updateProfiles();
|
onPressed: () {
|
||||||
},
|
final profiles = globalState.config.profiles;
|
||||||
icon: const Icon(Icons.sync),
|
showSheet(
|
||||||
),
|
title: appLocalizations.profilesSort,
|
||||||
IconButton(
|
context: context,
|
||||||
onPressed: () {
|
body: SizedBox(
|
||||||
final profiles = globalState.appController.config.profiles;
|
height: 400,
|
||||||
showSheet(
|
child: ReorderableProfiles(profiles: profiles),
|
||||||
title: appLocalizations.profilesSort,
|
),
|
||||||
context: context,
|
);
|
||||||
body: SizedBox(
|
},
|
||||||
height: 400,
|
icon: const Icon(Icons.sort),
|
||||||
child: ReorderableProfiles(profiles: profiles),
|
iconSize: 26,
|
||||||
),
|
),
|
||||||
);
|
];
|
||||||
},
|
|
||||||
icon: const Icon(Icons.sort),
|
@override
|
||||||
iconSize: 26,
|
Widget? get floatingActionButton => FloatingActionButton(
|
||||||
),
|
heroTag: null,
|
||||||
];
|
onPressed: _handleShowAddExtendPage,
|
||||||
commonScaffoldState?.floatingActionButton = FloatingActionButton(
|
child: const Icon(
|
||||||
heroTag: null,
|
Icons.add,
|
||||||
onPressed: _handleShowAddExtendPage,
|
),
|
||||||
child: const Icon(
|
);
|
||||||
Icons.add,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActiveBuilder(
|
return Consumer(
|
||||||
label: "profiles",
|
builder: (_, ref, __) {
|
||||||
builder: (isCurrent, child) {
|
ref.listenManual(
|
||||||
if (isCurrent) {
|
isCurrentPageProvider(PageLabel.profiles),
|
||||||
_initScaffold();
|
(prev, next) {
|
||||||
}
|
if (prev != next && next == true) {
|
||||||
return child!;
|
initPageState();
|
||||||
},
|
}
|
||||||
child: Selector2<AppState, Config, ProfilesSelectorState>(
|
},
|
||||||
selector: (_, appState, config) => ProfilesSelectorState(
|
fireImmediately: true,
|
||||||
profiles: config.profiles,
|
);
|
||||||
currentProfileId: config.currentProfileId,
|
final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
|
||||||
columns: other.getProfilesColumns(appState.viewWidth),
|
if (profilesSelectorState.profiles.isEmpty) {
|
||||||
),
|
return NullStatus(
|
||||||
builder: (context, state, child) {
|
label: appLocalizations.nullProfileDesc,
|
||||||
if (state.profiles.isEmpty) {
|
|
||||||
return NullStatus(
|
|
||||||
label: appLocalizations.nullProfileDesc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 88,
|
|
||||||
),
|
|
||||||
child: Grid(
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
crossAxisCount: state.columns,
|
|
||||||
children: [
|
|
||||||
for (int i = 0; i < state.profiles.length; i++)
|
|
||||||
GridItem(
|
|
||||||
child: ProfileItem(
|
|
||||||
key: Key(state.profiles[i].id),
|
|
||||||
profile: state.profiles[i],
|
|
||||||
groupValue: state.currentProfileId,
|
|
||||||
onChanged: globalState.appController.changeProfile,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: 88,
|
||||||
|
),
|
||||||
|
child: Grid(
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
crossAxisCount: profilesSelectorState.columns,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
|
||||||
|
GridItem(
|
||||||
|
child: ProfileItem(
|
||||||
|
key: Key(profilesSelectorState.profiles[i].id),
|
||||||
|
profile: profilesSelectorState.profiles[i],
|
||||||
|
groupValue: profilesSelectorState.currentProfileId,
|
||||||
|
onChanged: (profileId) {
|
||||||
|
ref.read(currentProfileIdProvider.notifier).value =
|
||||||
|
profileId;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,18 +169,16 @@ class ProfileItem extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_handleDeleteProfile(BuildContext context) async {
|
_handleDeleteProfile(BuildContext context) async {
|
||||||
globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
title: appLocalizations.tip,
|
title: appLocalizations.tip,
|
||||||
message: TextSpan(
|
message: TextSpan(
|
||||||
text: appLocalizations.deleteProfileTip,
|
text: appLocalizations.deleteProfileTip,
|
||||||
),
|
),
|
||||||
onTab: () async {
|
|
||||||
await globalState.appController.deleteProfile(profile.id);
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
if (res != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await globalState.appController.deleteProfile(profile.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleUpdateProfile() async {
|
_handleUpdateProfile() async {
|
||||||
@@ -205,18 +187,17 @@ class ProfileItem extends StatelessWidget {
|
|||||||
|
|
||||||
Future updateProfile() async {
|
Future updateProfile() async {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final config = appController.config;
|
|
||||||
if (profile.type == ProfileType.file) return;
|
if (profile.type == ProfileType.file) return;
|
||||||
await globalState.safeRun(silence: false, () async {
|
await globalState.safeRun(silence: false, () async {
|
||||||
try {
|
try {
|
||||||
config.setProfile(
|
appController.setProfile(
|
||||||
profile.copyWith(
|
profile.copyWith(
|
||||||
isUpdating: true,
|
isUpdating: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await appController.updateProfile(profile);
|
await appController.updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
config.setProfile(
|
appController.setProfile(
|
||||||
profile.copyWith(
|
profile.copyWith(
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
),
|
),
|
||||||
@@ -266,8 +247,48 @@ class ProfileItem extends StatelessWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _handleCopyLink(BuildContext context) async {
|
||||||
|
// await Clipboard.setData(
|
||||||
|
// ClipboardData(
|
||||||
|
// text: profile.url,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// if (context.mounted) {
|
||||||
|
// context.showNotifier(appLocalizations.copySuccess);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
_handleExportFile(BuildContext context) async {
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||||
|
() async {
|
||||||
|
final file = await profile.getFile();
|
||||||
|
final value = await picker.saveFile(
|
||||||
|
profile.label ?? profile.id,
|
||||||
|
file.readAsBytesSync(),
|
||||||
|
);
|
||||||
|
if (value == null) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
);
|
||||||
|
if (res == true && context.mounted) {
|
||||||
|
context.showNotifier(appLocalizations.exportSuccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _handlePushCustomPage(BuildContext context, String id) {
|
||||||
|
// BaseNavigator.push(
|
||||||
|
// context,
|
||||||
|
// CustomProfile(
|
||||||
|
// profileId: id,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final key = GlobalKey<CommonPopupBoxState>();
|
||||||
return CommonCard(
|
return CommonCard(
|
||||||
isSelected: profile.id == groupValue,
|
isSelected: profile.id == groupValue,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -286,46 +307,69 @@ class ProfileItem extends StatelessWidget {
|
|||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
)
|
)
|
||||||
: CommonPopupMenu<ProfileActions>(
|
: CommonPopupBox(
|
||||||
icon: Icon(Icons.more_vert),
|
key: key,
|
||||||
items: [
|
popup: CommonPopupMenu(
|
||||||
CommonPopupMenuItem(
|
items: [
|
||||||
action: ProfileActions.edit,
|
ActionItemData(
|
||||||
label: appLocalizations.edit,
|
icon: Icons.edit_outlined,
|
||||||
iconData: Icons.edit,
|
label: appLocalizations.edit,
|
||||||
),
|
onPressed: () {
|
||||||
if (profile.type == ProfileType.url)
|
_handleShowEditExtendPage(context);
|
||||||
CommonPopupMenuItem(
|
},
|
||||||
action: ProfileActions.update,
|
|
||||||
label: appLocalizations.update,
|
|
||||||
iconData: Icons.sync,
|
|
||||||
),
|
),
|
||||||
CommonPopupMenuItem(
|
if (profile.type == ProfileType.url) ...[
|
||||||
action: ProfileActions.delete,
|
ActionItemData(
|
||||||
label: appLocalizations.delete,
|
icon: Icons.sync_alt_sharp,
|
||||||
iconData: Icons.delete,
|
label: appLocalizations.sync,
|
||||||
),
|
onPressed: () {
|
||||||
],
|
_handleUpdateProfile();
|
||||||
onSelected: (ProfileActions? action) async {
|
},
|
||||||
switch (action) {
|
),
|
||||||
case ProfileActions.edit:
|
// ActionItemData(
|
||||||
_handleShowEditExtendPage(context);
|
// icon: Icons.copy,
|
||||||
break;
|
// label: appLocalizations.copyLink,
|
||||||
case ProfileActions.delete:
|
// onPressed: () {
|
||||||
_handleDeleteProfile(context);
|
// _handleCopyLink(context);
|
||||||
break;
|
// },
|
||||||
case ProfileActions.update:
|
// ),
|
||||||
_handleUpdateProfile();
|
],
|
||||||
break;
|
// ActionItemData(
|
||||||
case null:
|
// icon: Icons.extension_outlined,
|
||||||
break;
|
// label: "自定义",
|
||||||
}
|
// onPressed: () {
|
||||||
},
|
// _handlePushCustomPage(context, profile.id);
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
ActionItemData(
|
||||||
|
icon: Icons.file_copy_outlined,
|
||||||
|
label: appLocalizations.exportFile,
|
||||||
|
onPressed: () {
|
||||||
|
_handleExportFile(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ActionItemData(
|
||||||
|
icon: Icons.delete_outlined,
|
||||||
|
iconSize: 20,
|
||||||
|
label: appLocalizations.delete,
|
||||||
|
onPressed: () {
|
||||||
|
_handleDeleteProfile(context);
|
||||||
|
},
|
||||||
|
type: ActionType.danger,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
target: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
key.currentState?.pop();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Container(
|
title: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -462,7 +506,7 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
|||||||
child: FilledButton.tonal(
|
child: FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
globalState.appController.config.profiles = profiles;
|
globalState.appController.setProfiles(profiles);
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(
|
padding: WidgetStateProperty.all(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user