Compare commits

...

12 Commits

Author SHA1 Message Date
chen08209
a4b5f4abdb Fix the problem that ui can't be synchronized when android vpn is occupied by an external
Override default socksPort,port
2024-07-15 13:47:06 +08:00
chen08209
aa4ffbe4fb Fix fab issues 2024-07-14 01:37:44 +08:00
chen08209
3d25298639 Update version 2024-07-14 00:05:42 +08:00
chen08209
1765576d09 Fix the problem that vpn cannot be started in some cases
Fix the problem that geodata url does not take effect
2024-07-14 00:01:18 +08:00
chen08209
2dd45062f1 Update ua
Fix change outbound mode without check ip issues
2024-07-13 20:43:28 +08:00
chen08209
c6407984ac Separate android ui and vpn 2024-07-13 20:43:21 +08:00
chen08209
53af86238e Fix url validate issues 2
Add android hidden from the recent task

Add geoip file

Support modify geoData URL
2024-07-13 20:43:18 +08:00
chen08209
b20d9edec2 Fix url validate issues
Fix check ip performance problem

Optimize resources page
2024-07-07 13:37:08 +08:00
chen08209
5c3a0c576d Add ua selector
Support modify test url

Optimize android proxy

Fix the error that async proxy provider could not selected the proxy
2024-07-04 09:55:06 +08:00
chen08209
6dcb466fd3 Fix android proxy error 2024-07-01 20:57:24 +08:00
chen08209
acbcec358b Fix submit error 2024-07-01 19:51:11 +08:00
chen08209
a923549ddf Add windows tun
Optimize android proxy

Optimize change profile

Update application ua

Optimize delay test
2024-07-01 19:41:57 +08:00
90 changed files with 3853 additions and 2035 deletions

View File

@@ -21,11 +21,25 @@ jobs:
os: macos-13 os: macos-13
steps: steps:
- name: Setup Mingw64
if: startsWith(matrix.platform,'windows')
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
install: mingw-w64-x86_64-gcc
update: true
- name: Set Mingw64 Env
if: startsWith(matrix.platform,'windows')
run: |
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Check Matrix - name: Check Matrix
run: | run: |
echo "Running on ${{ matrix.os }}" echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}" echo "Arch: ${{ runner.arch }}"
gcc --version gcc --version
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -52,10 +66,10 @@ jobs:
if: startsWith(matrix.platform,'android') if: startsWith(matrix.platform,'android')
run: | run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5

View File

@@ -22,8 +22,10 @@
<application <application
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true" android:extractNativeLibs="true"
android:label="FlClash"> android:label="FlClash"
tools:targetApi="n">
<activity <activity
android:name="com.follow.clash.MainActivity" android:name="com.follow.clash.MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@@ -1,10 +1,15 @@
package com.follow.clash package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import java.util.Date import io.flutter.embedding.engine.dart.DartExecutor
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
enum class RunState { enum class RunState {
START, START,
@@ -12,16 +17,46 @@ enum class RunState {
STOP STOP
} }
class GlobalState {
companion object {
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var runTime: Date? = null
var flutterEngine: FlutterEngine? = null
fun getCurrentTilePlugin(): TilePlugin? =
flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
fun getCurrentAppPlugin(): AppPlugin? = object GlobalState {
flutterEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
private val lock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null
fun getCurrentAppPlugin(): AppPlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun getCurrentTitlePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null
}
fun initServiceEngine(context: Context) {
if (serviceEngine != null) return
lock.withLock {
if (serviceEngine != null) return
serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(ProxyPlugin())
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
)
serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService,
)
}
} }
} }

View File

@@ -10,7 +10,6 @@ import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GlobalState.flutterEngine?.destroy()
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin()) flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ProxyPlugin()) flutterEngine.plugins.add(ProxyPlugin())

View File

@@ -2,27 +2,27 @@ 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.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toBitmap import com.follow.clash.GlobalState
import com.follow.clash.extensions.getBase64 import com.follow.clash.extensions.getBase64
import com.follow.clash.extensions.getInetSocketAddress
import com.follow.clash.extensions.getProtocol import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Process
import com.follow.clash.models.Package import com.follow.clash.models.Package
import com.follow.clash.models.Process
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.InetSocketAddress import java.net.InetSocketAddress
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var activity: Activity? = null private var activity: Activity? = null
@@ -47,30 +48,41 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private val iconMap = mutableMapOf<String, String?>() private val iconMap = mutableMapOf<String, String?>()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext; context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
scope.cancel()
} }
private fun tip(message: String?) { private fun tip(message: String?) {
if (toast != null) { if(GlobalState.flutterEngine == null){
toast!!.cancel() if (toast != null) {
toast!!.cancel()
}
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
} }
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { when (call.method) {
"moveTaskToBack" -> { "moveTaskToBack" -> {
activity?.moveTaskToBack(true) activity?.moveTaskToBack(true)
result.success(true); result.success(true);
} }
"updateExcludeFromRecents" -> {
val value = call.argument<Boolean>("value")
updateExcludeFromRecents(value)
result.success(true);
}
"getPackages" -> { "getPackages" -> {
scope.launch { scope.launch {
result.success(getPackages()) result.success(getPackages())
@@ -115,7 +127,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
scope.launch { scope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q){ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null) result.success(null)
return@withContext return@withContext
} }
@@ -158,6 +170,24 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
} }
private fun updateExcludeFromRecents(value: Boolean?) {
if (context == null) return
val am = getSystemService(context!!, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId
} else {
it.taskInfo.id == activity?.taskId
}
}
when (value) {
true -> task?.setExcludeFromRecents(value)
false -> task?.setExcludeFromRecents(value)
null -> task?.setExcludeFromRecents(false)
}
}
private suspend fun getPackageIcon(packageName: String): String? { private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context?.packageManager val packageManager = context?.packageManager
if (iconMap[packageName] == null) { if (iconMap[packageName] == null) {
@@ -197,11 +227,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity; activity = binding.activity;
scope = CoroutineScope(Dispatchers.Default)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
activity = null; activity = null
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
@@ -210,7 +239,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
channel.invokeMethod("exit", null) channel.invokeMethod("exit", null)
scope.cancel() activity = null
activity = null;
} }
} }

View File

@@ -1,7 +1,6 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -9,8 +8,7 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.VpnService import android.net.VpnService
import android.os.Build.VERSION import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.IBinder import android.os.IBinder
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -25,38 +23,36 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.util.Date
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private lateinit var flutterMethodChannel: MethodChannel
val VPN_PERMISSION_REQUEST_CODE = 1001 val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002 val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private lateinit var flutterMethodChannel: MethodChannel
private var activity: Activity? = null private var activity: Activity? = null
private var context: Context? = null private var context: Context? = null
private var flClashVpnService: FlClashVpnService? = null private var flClashVpnService: FlClashVpnService? = null
private var isBound = false private var port: Int = 7890
private var port: Int? = null
private var props: Props? = null private var props: Props? = null
private lateinit var title: String private var isBlockNotification: Boolean = false
private lateinit var content: String private var isStart: Boolean = false
var isBlockNotification: Boolean = false
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FlClashVpnService.LocalBinder val binder = service as FlClashVpnService.LocalBinder
flClashVpnService = binder.getService() flClashVpnService = binder.getService()
port?.let { startVpn(it) } if (isStart) {
isBound = true startVpn()
} else {
flClashVpnService?.initServiceEngine()
}
} }
override fun onServiceDisconnected(arg0: ComponentName) { override fun onServiceDisconnected(arg: ComponentName) {
flClashVpnService = null flClashVpnService = null
isBound = false
} }
} }
@@ -71,21 +67,29 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"StartProxy" -> { "initService" -> {
port = call.argument<Int>("port") isStart = false
val args = call.argument<String>("args") initService()
props = requestNotificationsPermission()
if (args != null) Gson().fromJson(args, Props::class.java) else null
handleStartVpn()
result.success(true) result.success(true)
} }
"StopProxy" -> { "startProxy" -> {
isStart = true
port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
startVpn()
result.success(true)
}
"stopProxy" -> {
stopVpn() stopVpn()
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) {
flClashVpnService?.protect(fd) flClashVpnService?.protect(fd)
@@ -95,14 +99,10 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
} }
} }
"GetRunTimeStamp" -> {
result.success(GlobalState.runTime?.time)
}
"startForeground" -> { "startForeground" -> {
title = call.argument<String>("title") as String val title = call.argument<String>("title") as String
content = call.argument<String>("content") as String val content = call.argument<String>("content") as String
requestNotificationsPermission() startForeground(title, content)
result.success(true) result.success(true)
} }
@@ -111,66 +111,41 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
} }
} }
private fun handleStartVpn() { private fun initService() {
val intent = VpnService.prepare(context) val intent = VpnService.prepare(context)
if (intent != null) { if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
} else { } else {
bindService() if (flClashVpnService != null) {
flClashVpnService!!.initServiceEngine()
} else {
bindService()
}
} }
} }
private fun startVpn(port: Int) { private fun startVpn() {
if (GlobalState.runState.value == RunState.START) return; if (flClashVpnService == null) {
flClashVpnService?.start(port, props) bindService()
return
}
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START GlobalState.runState.value = RunState.START
GlobalState.runTime = Date() flutterMethodChannel.invokeMethod("started", flClashVpnService?.start(port, props))
startAfter()
} }
private fun stopVpn() { private fun stopVpn() {
if (GlobalState.runState.value == RunState.STOP) return if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashVpnService?.stop() flClashVpnService?.stop()
unbindService() GlobalState.destroyServiceEngine()
GlobalState.runState.value = RunState.STOP;
GlobalState.runTime = null;
} }
@SuppressLint("ForegroundServiceType") private fun startForeground(title: String, content: String) {
private fun startForeground() {
if (GlobalState.runState.value != RunState.START) return if (GlobalState.runState.value != RunState.START) return
flClashVpnService?.startForeground(title, content) flClashVpnService?.startForeground(title, content)
} }
private fun requestNotificationsPermission() {
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
val permission = context?.let {
ContextCompat.checkSelfPermission(
it,
Manifest.permission.POST_NOTIFICATIONS
)
}
if (permission == PackageManager.PERMISSION_GRANTED) {
startForeground()
} else {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
} else {
startForeground()
}
}
private fun startAfter() {
flutterMethodChannel.invokeMethod("startAfter", flClashVpnService?.fd)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
binding.addActivityResultListener(::onActivityResult) binding.addActivityResultListener(::onActivityResult)
@@ -185,7 +160,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
stopVpn() stopVpn()
} }
} }
return true; return true
} }
private fun onRequestPermissionsResultListener( private fun onRequestPermissionsResultListener(
@@ -195,18 +170,32 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
): Boolean { ): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true isBlockNotification = true
if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startForeground()
}
}
} }
return false; return false
} }
private fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = context?.let {
ContextCompat.checkSelfPermission(
it,
Manifest.permission.POST_NOTIFICATIONS
)
}
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activity == null) return
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
}
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
activity = null; activity = null
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
@@ -214,7 +203,6 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
stopVpn()
activity = null activity = null
} }
@@ -222,11 +210,4 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
val intent = Intent(context, FlClashVpnService::class.java) val intent = Intent(context, FlClashVpnService::class.java)
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE) context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
private fun unbindService() {
if (isBound) {
context?.unbindService(connection)
isBound = false
}
}
} }

View File

@@ -1,3 +1,4 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin

View File

@@ -1,9 +1,9 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -11,14 +11,9 @@ import androidx.lifecycle.Observer
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.TempActivity import com.follow.clash.TempActivity
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
@RequiresApi(Build.VERSION_CODES.N)
class FlClashTileService : TileService() { class FlClashTileService : TileService() {
private val observer = Observer<RunState> { runState -> private val observer = Observer<RunState> { runState ->
@@ -62,42 +57,25 @@ class FlClashTileService : TileService() {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent) startActivityAndCollapse(pendingIntent)
}else{ } else {
startActivityAndCollapse(intent) startActivityAndCollapse(intent)
} }
} }
private var flutterEngine: FlutterEngine? = null;
private fun initFlutterEngine() {
flutterEngine = FlutterEngine(this)
flutterEngine?.plugins?.add(ProxyPlugin())
flutterEngine?.plugins?.add(TilePlugin())
flutterEngine?.plugins?.add(AppPlugin())
GlobalState.flutterEngine = flutterEngine
if (flutterEngine?.dartExecutor?.isExecutingDart != true) {
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
)
flutterEngine?.dartExecutor?.executeDartEntrypoint(vpnService)
}
}
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
activityTransfer() activityTransfer()
val currentTilePlugin = GlobalState.getCurrentTilePlugin()
if (GlobalState.runState.value == RunState.STOP) { if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING GlobalState.runState.value = RunState.PENDING
if(currentTilePlugin == null){ val titlePlugin = GlobalState.getCurrentTitlePlugin()
initFlutterEngine() if (titlePlugin != null) {
}else{ titlePlugin.handleStart()
currentTilePlugin.handleStart() } else {
GlobalState.initServiceEngine(applicationContext)
} }
} else if(GlobalState.runState.value == RunState.START){ } else if (GlobalState.runState.value == RunState.START) {
GlobalState.runState.value = RunState.PENDING GlobalState.runState.value = RunState.PENDING
currentTilePlugin?.handleStop() GlobalState.getCurrentTitlePlugin()?.handleStop()
} }
} }

View File

@@ -11,20 +11,22 @@ import android.net.VpnService
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
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
import com.follow.clash.models.AccessControlMode import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.Props import com.follow.clash.models.Props
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlClashVpnService : VpnService() { class FlClashVpnService : VpnService() {
private val CHANNEL = "FlClash" private val CHANNEL = "FlClash"
var fd: Int? = null
private val notificationId: Int = 1 private val notificationId: Int = 1
private val passList = listOf( private val passList = listOf(
@@ -47,12 +49,13 @@ class FlClashVpnService : VpnService() {
"192.168.*" "192.168.*"
) )
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onCreate() {
return START_STICKY super.onCreate()
initServiceEngine()
} }
fun start(port: Int, props: Props?) { fun start(port: Int, props: Props?): Int? {
fd = with(Builder()) { return with(Builder()) {
addAddress("172.16.0.1", 30) addAddress("172.16.0.1", 30)
setMtu(9000) setMtu(9000)
addRoute("0.0.0.0", 0) addRoute("0.0.0.0", 0)
@@ -98,7 +101,6 @@ class FlClashVpnService : VpnService() {
stopForeground() stopForeground()
} }
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
@@ -117,7 +119,6 @@ class FlClashVpnService : VpnService() {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
} }
with(NotificationCompat.Builder(this, CHANNEL)) { with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name) setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash") setContentTitle("FlClash")
@@ -130,10 +131,14 @@ class FlClashVpnService : VpnService() {
setOngoing(true) setOngoing(true)
setShowWhen(false) setShowWhen(false)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setAutoCancel(true); setAutoCancel(true)
} }
} }
fun initServiceEngine() {
GlobalState.initServiceEngine(applicationContext)
}
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc() GlobalState.getCurrentAppPlugin()?.requestGc()
@@ -168,14 +173,26 @@ class FlClashVpnService : VpnService() {
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getService(): FlClashVpnService = this@FlClashVpnService fun getService(): FlClashVpnService = this@FlClashVpnService
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
try {
return super.onTransact(code, data, reply, flags)
} catch (e: RemoteException) {
throw e
}
}
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
return binder return binder
} }
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
GlobalState.getCurrentTilePlugin()?.handleStop(); GlobalState.getCurrentTitlePlugin()?.handleStop()
return super.onUnbind(intent) return super.onUnbind(intent)
} }

View File

@@ -7,4 +7,8 @@
<certificates src="user" /> <certificates src="user" />
</trust-anchors> </trust-anchors>
</base-config> </base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config> </network-security-config>

BIN
assets/data/GeoIP.dat Normal file

Binary file not shown.

View File

@@ -4,6 +4,7 @@ import "C"
import ( import (
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
ap "github.com/metacubex/mihomo/adapter/provider" ap "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
@@ -20,6 +21,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
) )
@@ -59,11 +61,17 @@ type ruleProviderSchema struct {
Interval int `provider:"interval,omitempty"` Interval int `provider:"interval,omitempty"`
} }
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
}
type GenerateConfigParams struct { type GenerateConfigParams struct {
ProfilePath *string `json:"profile-path"` ProfilePath *string `json:"profile-path"`
Config *config.RawConfig `json:"config" ` Config config.RawConfig `json:"config" `
IsPatch *bool `json:"is-patch"` Params ConfigExtendedParams `json:"params"`
IsCompatible *bool `json:"is-compatible"`
} }
type ChangeProxyParams struct { type ChangeProxyParams struct {
@@ -76,26 +84,11 @@ type TestDelayParams struct {
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
} }
type Delay struct {
Name string `json:"name"`
Value int32 `json:"value"`
}
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type ProcessMapItem struct { type ProcessMapItem struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Value string `json:"value"` Value string `json:"value"`
} }
type Now struct {
Name string `json:"name"`
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"`
@@ -170,9 +163,9 @@ func getRawConfigWithPath(path *string) *config.RawConfig {
} }
} }
func decorationConfig(profilePath *string, cfg config.RawConfig, compatible bool) *config.RawConfig { func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
prof := getRawConfigWithPath(profilePath) prof := getRawConfigWithPath(profilePath)
overwriteConfig(prof, cfg, compatible) overwriteConfig(prof, cfg)
return prof return prof
} }
@@ -322,14 +315,14 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
*rule = computedRule *rule = computedRule
} }
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) { func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
targetConfig.ExternalController = patchConfig.ExternalController targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = "" targetConfig.ExternalUI = ""
targetConfig.Interface = "" targetConfig.Interface = ""
targetConfig.ExternalUIURL = "" targetConfig.ExternalUIURL = ""
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
targetConfig.GeodataMode = false //targetConfig.GeodataMode = false
targetConfig.IPv6 = patchConfig.IPv6 targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0 targetConfig.Port = 0
@@ -340,10 +333,12 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Mode = patchConfig.Mode targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable targetConfig.Tun.Enable = patchConfig.Tun.Enable
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.GeodataLoader = patchConfig.GeodataLoader targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA
if targetConfig.DNS.Enable == false { if targetConfig.DNS.Enable == false {
targetConfig.DNS = patchConfig.DNS targetConfig.DNS = patchConfig.DNS
} }
@@ -352,7 +347,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
//} else if runtime.GOOS == "windows" { //} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder) // targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//} //}
if compatible == false { if configParams.IsCompatible == false {
targetConfig.ProxyProvider = make(map[string]map[string]any) targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any) targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule) generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
@@ -388,16 +383,48 @@ func patchConfig(general *config.General) {
resolver.DisableIPv6 = !general.IPv6 resolver.DisableIPv6 = !general.IPv6
} }
func applyConfig(isPatch bool) { func patchSelectGroup() {
mapping := configParams.SelectedMap
if mapping == nil {
return
}
for name, proxy := range tunnel.ProxiesWithProviders() {
outbound, ok := proxy.(*adapter.Proxy)
if !ok {
continue
}
selector, ok := outbound.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
continue
}
selected, exist := mapping[name]
if !exist {
continue
}
selector.ForceSet(selected)
}
}
var applyLock sync.Mutex
func applyConfig() {
applyLock.Lock()
defer applyLock.Unlock()
cfg, err := config.ParseRawConfig(currentConfig) cfg, err := config.ParseRawConfig(currentConfig)
if err != nil { if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig()) cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
} }
if isPatch { if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
if configParams.IsPatch {
patchConfig(cfg.General) patchConfig(cfg.General)
} else { } else {
executor.Shutdown()
runtime.GC() runtime.GC()
hub.UltraApplyConfig(cfg, true) hub.UltraApplyConfig(cfg, true)
patchSelectGroup()
} }
} }

View File

@@ -25,7 +25,7 @@ func InitDartApi(api unsafe.Pointer) {
} }
} }
func SendToPort(port int64, msg string) { func SendToPort(port int64, msg string) bool {
var obj C.Dart_CObject var obj C.Dart_CObject
obj._type = C.Dart_CObject_kString obj._type = C.Dart_CObject_kString
msgString := C.CString(msg) msgString := C.CString(msg)
@@ -34,6 +34,7 @@ func SendToPort(port int64, msg string) {
*(**C.char)(ptr) = msgString *(**C.char)(ptr) = msgString
isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj)
if !isSuccess { if !isSuccess {
fmt.Println("ERROR: post to port ", port, " failed", msg) return false
} }
return true
} }

View File

@@ -1,38 +0,0 @@
package dart_bridge
import "encoding/json"
var Port *int64
type MessageType string
const (
Log MessageType = "log"
Tun MessageType = "tun"
Delay MessageType = "delay"
Now MessageType = "now"
Process MessageType = "process"
Request MessageType = "request"
Run MessageType = "run"
)
type Message struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
}
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func SendMessage(message Message) {
if Port == nil {
return
}
s, err := message.Json()
if err != nil {
return
}
SendToPort(*Port, s)
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
@@ -31,8 +32,12 @@ import (
var currentConfig = config.DefaultRawConfig() var currentConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
var isInit = false var isInit = false
var currentProfileName = ""
//export initClash //export initClash
func initClash(homeDirStr *C.char) bool { func initClash(homeDirStr *C.char) bool {
if !isInit { if !isInit {
@@ -71,6 +76,16 @@ func forceGc() {
}() }()
} }
//export setCurrentProfileName
func setCurrentProfileName(s *C.char) {
currentProfileName = C.GoString(s)
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(currentProfileName)
}
//export validateConfig //export validateConfig
func validateConfig(s *C.char, port C.longlong) { func validateConfig(s *C.char, port C.longlong) {
i := int64(port) i := int64(port)
@@ -96,13 +111,10 @@ func updateConfig(s *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
prof := decorationConfig(params.ProfilePath, *params.Config, *params.IsCompatible) configParams = params.Params
prof := decorationConfig(params.ProfilePath, params.Config)
currentConfig = prof currentConfig = prof
if *params.IsPatch { applyConfig()
applyConfig(true)
} else {
applyConfig(false)
}
bridge.SendToPort(i, "") bridge.SendToPort(i, "")
}() }()
} }
@@ -150,7 +162,7 @@ func getProxies() *C.char {
} }
//export changeProxy //export changeProxy
func changeProxy(s *C.char) bool { func changeProxy(s *C.char) {
paramsString := C.GoString(s) paramsString := C.GoString(s)
go func() { go func() {
var params = &ChangeProxyParams{} var params = &ChangeProxyParams{}
@@ -158,22 +170,24 @@ func changeProxy(s *C.char) bool {
if err != nil { if err != nil {
log.Infoln("Unmarshal ChangeProxyParams %v", err) log.Infoln("Unmarshal ChangeProxyParams %v", err)
} }
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders() proxies := tunnel.ProxiesWithProviders()
proxy := proxies[*params.GroupName] group, ok := proxies[groupName]
if proxy == nil { if !ok {
return return
} }
log.Infoln("change proxy %s", proxy.Name()) adapterProxy := group.(*adapter.Proxy)
adapterProxy := proxy.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector) selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector)
if !ok { if !ok {
return return
} }
if err := selector.Set(*params.ProxyName); err != nil {
return err = selector.Set(proxyName)
if err == nil {
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
} }
}() }()
return true
} }
//export getTraffic //export getTraffic
@@ -383,34 +397,44 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoIp": case "MMDB":
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString)) err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoSite":
err := mmdb.DownloadGeoSite(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN": case "ASN":
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString)) err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString))
if err != nil { if err != nil {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoIp":
err := geodata.DownloadGeoIP(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := geodata.DownloadGeoSite(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
} }
bridge.SendToPort(i, "") bridge.SendToPort(i, "")
}() }()
} }
//export initNativeApiBridge //export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer, port C.longlong) { func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api) bridge.InitDartApi(api)
}
//export initMessage
func initMessage(port C.longlong) {
i := int64(port) i := int64(port)
bridge.Port = &i Port = i
} }
//export freeCString //export freeCString
@@ -428,15 +452,21 @@ func init() {
} else { } else {
delayData.Value = int32(delay) delayData.Value = int32(delay)
} }
bridge.SendMessage(bridge.Message{ SendMessage(Message{
Type: bridge.Delay, Type: DelayMessage,
Data: delayData, Data: delayData,
}) })
} }
statistic.DefaultRequestNotify = func(c statistic.Tracker) { statistic.DefaultRequestNotify = func(c statistic.Tracker) {
bridge.SendMessage(bridge.Message{ SendMessage(Message{
Type: bridge.Request, Type: RequestMessage,
Data: c, Data: c,
}) })
} }
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
SendMessage(Message{
Type: LoadedMessage,
Data: providerName,
})
}
} }

View File

@@ -2,7 +2,6 @@ package main
import "C" import "C"
import ( import (
bridge "core/dart-bridge"
"github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
@@ -21,11 +20,11 @@ func startLog() {
if logData.LogLevel < log.Level() { if logData.LogLevel < log.Level() {
continue continue
} }
message := &bridge.Message{ message := &Message{
Type: bridge.Log, Type: LogMessage,
Data: logData, Data: logData,
} }
bridge.SendMessage(*message) SendMessage(*message)
} }
}() }()
} }

77
core/message.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
bridge "core/dart-bridge"
"encoding/json"
"github.com/metacubex/mihomo/constant"
)
var Port int64
var ServicePort int64
type MessageType string
const (
LogMessage MessageType = "log"
ProtectMessage MessageType = "protect"
DelayMessage MessageType = "delay"
ProcessMessage MessageType = "process"
RequestMessage MessageType = "request"
StartedMessage MessageType = "started"
LoadedMessage MessageType = "loaded"
)
type Delay struct {
Name string `json:"name"`
Value int32 `json:"value"`
}
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type Message struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
}
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
if handler, ok := messageHandlers[message.Type]; ok {
handler(s)
} else {
sendToPort(s)
}
}
var messageHandlers = map[MessageType]func(string) bool{
ProtectMessage: sendToServicePort,
ProcessMessage: sendToServicePort,
StartedMessage: conditionalSend,
LoadedMessage: conditionalSend,
}
func sendToPort(s string) bool {
return bridge.SendToPort(Port, s)
}
func sendToServicePort(s string) bool {
return bridge.SendToPort(ServicePort, s)
}
func conditionalSend(s string) bool {
isSuccess := sendToPort(s)
if !isSuccess {
return sendToServicePort(s)
}
return isSuccess
}

View File

@@ -4,7 +4,6 @@ package main
import "C" import "C"
import ( import (
bridge "core/dart-bridge"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/process"
@@ -43,8 +42,8 @@ func init() {
timeout := time.After(200 * time.Millisecond) timeout := time.After(200 * time.Millisecond)
bridge.SendMessage(bridge.Message{ SendMessage(Message{
Type: bridge.Process, Type: ProcessMessage,
Data: Process{ Data: Process{
Id: id, Id: id,
Metadata: metadata, Metadata: metadata,

View File

@@ -10,16 +10,36 @@ import (
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore" "golang.org/x/sync/semaphore"
"strconv"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
) )
var tunLock sync.Mutex var tunLock sync.Mutex
var tun *t.Tun var tun *t.Tun
var runTime *time.Time
type FdMap struct {
m sync.Map
}
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
}
var fdMap FdMap
//export startTUN //export startTUN
func startTUN(fd C.int) { func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
go func() { go func() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -28,6 +48,7 @@ func startTUN(fd C.int) {
tun.Close() tun.Close()
tun = nil tun = nil
} }
f := int(fd) f := int(fd)
gateway := "172.16.0.1/30" gateway := "172.16.0.1/30"
portal := "172.16.0.2" portal := "172.16.0.2"
@@ -45,15 +66,34 @@ func startTUN(fd C.int) {
tempTun.Closer = closer tempTun.Closer = closer
tun = tempTun tun = tempTun
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
}() }()
} }
//export getRunTime
func getRunTime() *C.char {
if runTime == nil {
return C.CString("")
}
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
}
//export stopTun //export stopTun
func stopTun() { func stopTun() {
go func() { go func() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
runTime = nil
if tun != nil { if tun != nil {
tun.Close() tun.Close()
tun = nil tun = nil
@@ -63,15 +103,58 @@ func stopTun() {
var errBlocked = errors.New("blocked") var errBlocked = errors.New("blocked")
//export setFdMap
func setFdMap(fd C.long) {
fdInt := int64(fd)
go func() {
fdMap.Store(fdInt)
}()
}
type Fd struct {
Id int64 `json:"id"`
Value int64 `json:"value"`
}
func markSocket(fd Fd) {
SendMessage(Message{
Type: ProtectMessage,
Data: fd,
})
}
var fdCounter int64 = 0
func init() { func init() {
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() {
return errBlocked return errBlocked
} }
return conn.Control(func(fd uintptr) { return conn.Control(func(fd uintptr) {
if tun != nil { if tun == nil {
tun.MarkSocket(int(fd)) return
time.Sleep(time.Millisecond * 100) }
fdInt := int64(fd)
timeout := time.After(100 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
markSocket(Fd{
Id: id,
Value: fdInt,
})
for {
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(10 * time.Millisecond)
}
} }
}) })
} }

View File

@@ -5,7 +5,6 @@ package tun
import "C" import "C"
import ( import (
"context" "context"
bridge "core/dart-bridge"
"encoding/binary" "encoding/binary"
"github.com/Kr328/tun2socket" "github.com/Kr328/tun2socket"
"github.com/Kr328/tun2socket/nat" "github.com/Kr328/tun2socket/nat"
@@ -19,7 +18,6 @@ import (
"io" "io"
"net" "net"
"os" "os"
"strconv"
"time" "time"
) )
@@ -186,19 +184,3 @@ func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
return stack, nil return stack, nil
} }
func (t *Tun) MarkSocket(fd int) {
_ = t.Limit.Acquire(context.Background(), 1)
defer t.Limit.Release(1)
if t.Closed {
return
}
message := &bridge.Message{
Type: bridge.Tun,
Data: strconv.Itoa(fd),
}
bridge.SendMessage(*message)
}

View File

@@ -115,7 +115,6 @@ class ApplicationState extends State<Application> {
lightColorScheme: lightDynamic, lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic, darkColorScheme: darkDynamic,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateSystemColorSchemes(systemColorSchemes); globalState.appController.updateSystemColorSchemes(systemColorSchemes);
}); });

View File

@@ -35,7 +35,6 @@ class ClashCore {
clashFFI = ClashFFI(lib); clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge( clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData, NativeApi.initializeApiDLData,
receiver.sendPort.nativePort,
); );
} }
@@ -95,6 +94,28 @@ class ClashCore {
return completer.future; return completer.future;
} }
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
}
setProfileName(String profileName) {
final profileNameChar = profileName.toNativeUtf8().cast<Char>();
clashFFI.setCurrentProfileName(
profileNameChar,
);
malloc.free(profileNameChar);
}
getProfileName() {
final currentProfileNameRaw = clashFFI.getCurrentProfileName();
final currentProfileName =
currentProfileNameRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileNameRaw);
return currentProfileName;
}
Future<List<Group>> getProxiesGroups() { Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies(); final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString(); final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
@@ -117,6 +138,7 @@ class ClashCore {
.map( .map(
(name) => proxies[name], (name) => proxies[name],
) )
.where((proxy) => proxy != null)
.toList(); .toList();
return group; return group;
}).toList(); }).toList();
@@ -164,12 +186,11 @@ class ClashCore {
return completer.future; return completer.future;
} }
bool changeProxy(ChangeProxyParams changeProxyParams) { changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams); final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>(); final paramsChar = params.toNativeUtf8().cast<Char>();
final isInit = clashFFI.changeProxy(paramsChar) == 1; clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar); malloc.free(paramsChar);
return isInit;
} }
Future<Delay> getDelay(String proxyName) { Future<Delay> getDelay(String proxyName) {
@@ -185,7 +206,8 @@ class ClashCore {
receiver.close(); receiver.close();
} }
}); });
final delayParamsChar = json.encode(delayParams).toNativeUtf8().cast<Char>(); final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay( clashFFI.asyncTestDelay(
delayParamsChar, delayParamsChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
@@ -241,8 +263,9 @@ class ClashCore {
clashFFI.stopLog(); clashFFI.stopLog();
} }
startTun(int fd) { startTun(int fd, int port) {
clashFFI.startTUN(fd); if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
} }
requestGc() { requestGc() {
@@ -254,16 +277,23 @@ class ClashCore {
} }
void setProcessMap(ProcessMapItem processMapItem) { void setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar = json.encode(processMapItem).toNativeUtf8().cast<Char>(); final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar); clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar); malloc.free(processMapItemChar);
} }
// DateTime? getRunTime() { void setFdMap(int fd) {
// final runTimeString = clashFFI.getRunTime().cast<Utf8>().toDartString(); clashFFI.setFdMap(fd);
// if (runTimeString.isEmpty) return null; }
// return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
// } DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
List<Connection> getConnections() { List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections(); final connectionsDataRaw = clashFFI.getConnections();

View File

@@ -5190,6 +5190,30 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>(); late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void setCurrentProfileName(
ffi.Pointer<ffi.Char> s,
) {
return _setCurrentProfileName(
s,
);
}
late final _setCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setCurrentProfileName');
late final _setCurrentProfileName = _setCurrentProfileNamePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName();
}
late final _getCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getCurrentProfileName');
late final _getCurrentProfileName =
_getCurrentProfileNamePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void validateConfig( void validateConfig(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
int port, int port,
@@ -5248,7 +5272,7 @@ class ClashFFI {
late final _getProxies = late final _getProxies =
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>(); _getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
int changeProxy( void changeProxy(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
) { ) {
return _changeProxy( return _changeProxy(
@@ -5257,10 +5281,10 @@ class ClashFFI {
} }
late final _changeProxyPtr = late final _changeProxyPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>( _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'changeProxy'); 'changeProxy');
late final _changeProxy = late final _changeProxy =
_changeProxyPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>(); _changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getTraffic() { ffi.Pointer<ffi.Char> getTraffic() {
return _getTraffic(); return _getTraffic();
@@ -5406,20 +5430,30 @@ class ClashFFI {
void initNativeApiBridge( void initNativeApiBridge(
ffi.Pointer<ffi.Void> api, ffi.Pointer<ffi.Void> api,
int port,
) { ) {
return _initNativeApiBridge( return _initNativeApiBridge(
api, api,
);
}
late final _initNativeApiBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void initMessage(
int port,
) {
return _initMessage(
port, port,
); );
} }
late final _initNativeApiBridgePtr = _lookup< late final _initMessagePtr =
ffi.NativeFunction< _lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
ffi.Void Function( 'initMessage');
ffi.Pointer<ffi.Void>, ffi.LongLong)>>('initNativeApiBridge'); late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>, int)>();
void freeCString( void freeCString(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
@@ -5467,15 +5501,28 @@ class ClashFFI {
void startTUN( 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)>>('startTUN'); _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
late final _startTUN = _startTUNPtr.asFunction<void Function(int)>(); 'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() { void stopTun() {
return _stopTun(); return _stopTun();
@@ -5484,6 +5531,18 @@ class ClashFFI {
late final _stopTunPtr = late final _stopTunPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>(); late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
} }
typedef va_list = ffi.Pointer<ffi.Char>; typedef va_list = ffi.Pointer<ffi.Char>;

View File

@@ -7,22 +7,6 @@ import 'package:flutter/foundation.dart';
import 'core.dart'; import 'core.dart';
abstract mixin class ClashMessageListener {
void onLog(Log log) {}
void onTun(String fd) {}
void onDelay(Delay delay) {}
void onProcess(Process process) {}
void onRequest(Connection connection) {}
void onNow(Now now) {}
void onRun(String runTime) {}
}
class ClashMessage { class ClashMessage {
StreamSubscription? subscription; StreamSubscription? subscription;
@@ -32,29 +16,23 @@ class ClashMessage {
subscription = null; subscription = null;
} }
subscription = ClashCore.receiver.listen((message) { subscription = ClashCore.receiver.listen((message) {
final m = Message.fromJson(json.decode(message)); final m = AppMessage.fromJson(json.decode(message));
for (final ClashMessageListener listener in _listeners) { for (final AppMessageListener listener in _listeners) {
switch (m.type) { switch (m.type) {
case MessageType.log: case AppMessageType.log:
listener.onLog(Log.fromJson(m.data)); listener.onLog(Log.fromJson(m.data));
break; break;
case MessageType.tun: case AppMessageType.delay:
listener.onTun(m.data);
break;
case MessageType.delay:
listener.onDelay(Delay.fromJson(m.data)); listener.onDelay(Delay.fromJson(m.data));
break; break;
case MessageType.process: case AppMessageType.request:
listener.onProcess(Process.fromJson(m.data));
break;
case MessageType.now:
listener.onNow(Now.fromJson(m.data));
break;
case MessageType.request:
listener.onRequest(Connection.fromJson(m.data)); listener.onRequest(Connection.fromJson(m.data));
break; break;
case MessageType.run: case AppMessageType.started:
listener.onRun(m.data); listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break; break;
} }
} }
@@ -63,18 +41,18 @@ class ClashMessage {
static final ClashMessage instance = ClashMessage._(); static final ClashMessage instance = ClashMessage._();
final ObserverList<ClashMessageListener> _listeners = final ObserverList<AppMessageListener> _listeners =
ObserverList<ClashMessageListener>(); ObserverList<AppMessageListener>();
bool get hasListeners { bool get hasListeners {
return _listeners.isNotEmpty; return _listeners.isNotEmpty;
} }
void addListener(ClashMessageListener listener) { void addListener(AppMessageListener listener) {
_listeners.add(listener); _listeners.add(listener);
} }
void removeListener(ClashMessageListener listener) { void removeListener(AppMessageListener listener) {
_listeners.remove(listener); _listeners.remove(listener);
} }
} }

View File

@@ -17,6 +17,7 @@ class ClashService {
} }
const geoFileNameList = [ const geoFileNameList = [
mmdbFileName, mmdbFileName,
geoIpFileName,
geoSiteFileName, geoSiteFileName,
asnFileName, asnFileName,
]; ];

View File

@@ -1,14 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
class Android { class Android {
init() async { init() async {
app?.onExit = () { app?.onExit = () {};
clashCore.shutdown();
exit(0);
};
} }
} }

View File

@@ -22,4 +22,6 @@ export 'string.dart';
export 'app_localizations.dart'; export 'app_localizations.dart';
export 'function.dart'; export 'function.dart';
export 'package.dart'; export 'package.dart';
export 'measure.dart'; export 'measure.dart';
export 'service.dart';
export 'iterable.dart';

View File

@@ -1,5 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/models/clash_config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const appName = "FlClash"; const appName = "FlClash";
@@ -10,8 +11,15 @@ const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
const defaultUpdateDuration = Duration(days: 1); const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb"; const mmdbFileName = "geoip.metadb";
const geoSiteFileName = "GeoSite.dat";
const asnFileName = "ASN.mmdb"; const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat";
const GeoXMap defaultGeoXMap = {
"mmdb":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":"https://github.com/xishang0128/geoip/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";
@@ -24,6 +32,7 @@ const maxMobileWidth = 600;
const maxLaptopWidth = 840; const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative"; const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard"; const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur( final filter = ImageFilter.blur(
sigmaX: 5, sigmaX: 5,
sigmaY: 5, sigmaY: 5,

13
lib/common/iterable.dart Normal file
View File

@@ -0,0 +1,13 @@
extension IterableExt<T> on Iterable<T> {
Iterable<T> separated(T separator) sync* {
final iterator = this.iterator;
if (!iterator.moveNext()) return;
yield iterator.current;
while (iterator.moveNext()) {
yield separator;
yield iterator.current;
}
}
}

View File

@@ -39,6 +39,9 @@ class Other {
} }
final diff = timeStamp / 1000; final diff = timeStamp / 1000;
final inHours = (diff / 3600).floor(); final inHours = (diff / 3600).floor();
if (inHours > 99) {
return "99:59:59";
}
final inMinutes = (diff / 60 % 60).floor(); final inMinutes = (diff / 60 % 60).floor();
final inSeconds = (diff % 60).floor(); final inSeconds = (diff % 60).floor();
@@ -171,7 +174,7 @@ class Other {
} }
List<String> parseReleaseBody(String? body) { List<String> parseReleaseBody(String? body) {
if(body == null) return []; if (body == null) return [];
const pattern = r'- (.+?)\. \[.+?\]'; const pattern = r'- (.+?)\. \[.+?\]';
final regex = RegExp(pattern); final regex = RegExp(pattern);
return regex return regex
@@ -181,7 +184,7 @@ class Other {
.toList(); .toList();
} }
ViewMode getViewMode(double viewWidth){ ViewMode getViewMode(double viewWidth) {
if (viewWidth <= maxMobileWidth) return ViewMode.mobile; if (viewWidth <= maxMobileWidth) return ViewMode.mobile;
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop; if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop; return ViewMode.desktop;

View File

@@ -1,22 +1,13 @@
import 'dart:async'; import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
class AppPackage{ import 'common.dart';
static AppPackage? _instance; extension PackageInfoExtension on PackageInfo {
Completer<PackageInfo> packageInfoCompleter = Completer(); String get ua => [
"$appName/v$version",
AppPackage._internal() { "clash-verge",
PackageInfo.fromPlatform().then( "Platform/${Platform.operatingSystem}",
(value) => packageInfoCompleter.complete(value), ].join(" ");
);
}
factory AppPackage() {
_instance ??= AppPackage._internal();
return _instance!;
}
} }
final appPackage = AppPackage();

View File

@@ -1,29 +1,22 @@
import 'dart:io';
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';
class Picker { class Picker {
Future<PlatformFile?> pickerConfigFile() async { Future<PlatformFile?> pickerConfigFile() async {
FilePickerResult? filePickerResult; final filePickerResult = await FilePicker.platform.pickFiles(
if (Platform.isAndroid) { withData: true,
filePickerResult = await FilePicker.platform.pickFiles( allowMultiple: false,
withData: true, );
allowMultiple: false, return filePickerResult?.files.first;
); }
} else {
filePickerResult = await FilePicker.platform.pickFiles( Future<PlatformFile?> pickerGeoDataFile() async {
withData: true, final filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.custom, withData: true,
allowedExtensions: ['yaml', 'txt', 'conf'], allowMultiple: false,
); );
} return filePickerResult?.files.first;
final file = filePickerResult?.files.first;
if (file == null) {
return null;
}
return file;
} }
Future<String?> pickerConfigQRCode() async { Future<String?> pickerConfigQRCode() async {

View File

@@ -28,20 +28,6 @@ class ProxyManager {
return await (_proxy as Proxy).updateStartTime(); return await (_proxy as Proxy).updateStartTime();
} }
Future<bool?> setProtect(int fd) async {
if (_proxy is! Proxy) return null;
return await (_proxy as Proxy).setProtect(fd);
}
Future<bool?> startForeground({
required String title,
required String content,
}) async {
if (_proxy is! Proxy) return null;
return await (_proxy as Proxy)
.startForeground(title: title, content: content);
}
factory ProxyManager() { factory ProxyManager() {
_instance ??= ProxyManager._internal(); _instance ??= ProxyManager._internal();
return _instance!; return _instance!;

View File

@@ -12,17 +12,18 @@ class Request {
bool _isStart = false; bool _isStart = false;
Request() { Request() {
_dio = Dio( _dio = Dio();
BaseOptions( _dio.options = BaseOptions(
headers: {"User-Agent": coreName}, headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
return handler.next(options); // 继续请求
},
), ),
); );
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
return handler.next(options); // 继续请求
},
));
} }
_syncProxy() { _syncProxy() {
@@ -68,8 +69,7 @@ class Request {
if (response.statusCode != 200) return null; if (response.statusCode != 200) return null;
final data = response.data as Map<String, dynamic>; final data = response.data as Map<String, dynamic>;
final remoteVersion = data['tag_name']; final remoteVersion = data['tag_name'];
final packageInfo = await appPackage.packageInfoCompleter.future; final version = globalState.packageInfo.version;
final version = packageInfo.version;
final hasUpdate = final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return null; if (!hasUpdate) return null;

110
lib/common/service.dart Normal file
View File

@@ -0,0 +1,110 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
typedef CreateServiceNative = IntPtr Function(
IntPtr hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
Uint32 dwDesiredAccess,
Uint32 dwServiceType,
Uint32 dwStartType,
Uint32 dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
typedef CreateServiceDart = int Function(
int hSCManager,
Pointer<Utf16> lpServiceName,
Pointer<Utf16> lpDisplayName,
int dwDesiredAccess,
int dwServiceType,
int dwStartType,
int dwErrorControl,
Pointer<Utf16> lpBinaryPathName,
Pointer<Utf16> lpLoadOrderGroup,
Pointer<Uint32> lpdwTagId,
Pointer<Utf16> lpDependencies,
Pointer<Utf16> lpServiceStartName,
Pointer<Utf16> lpPassword,
);
const _SERVICE_ALL_ACCESS = 0xF003F;
const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
const _SERVICE_AUTO_START = 0x00000002;
const _SERVICE_ERROR_NORMAL = 0x00000001;
typedef GetLastErrorNative = Uint32 Function();
typedef GetLastErrorDart = int Function();
class Service {
static Service? _instance;
late DynamicLibrary _advapi32;
Service._internal() {
_advapi32 = DynamicLibrary.open('advapi32.dll');
}
factory Service() {
_instance ??= Service._internal();
return _instance!;
}
Future<void> createService() async {
final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
if (scManager == 0) return;
final serviceName = 'FlClash Service'.toNativeUtf16();
final displayName = 'FlClash Service'.toNativeUtf16();
final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
final createService =
_advapi32.lookupFunction<CreateServiceNative, CreateServiceDart>(
'CreateServiceW',
);
final getLastError = DynamicLibrary.open('kernel32.dll')
.lookupFunction<GetLastErrorNative, GetLastErrorDart>('GetLastError');
final serviceHandle = createService(
scManager,
serviceName,
displayName,
_SERVICE_ALL_ACCESS,
_SERVICE_WIN32_OWN_PROCESS,
_SERVICE_AUTO_START,
_SERVICE_ERROR_NORMAL,
binaryPathName,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
);
print("serviceHandle $serviceHandle");
final errorCode = GetLastError();
print('Error code: $errorCode');
final result = StartService(serviceHandle, 0, nullptr);
if (result == 0) {
print('Failed to start the service.');
} else {
print('Service started successfully.');
}
calloc.free(serviceName);
calloc.free(displayName);
calloc.free(binaryPathName);
}
}
final service = Platform.isWindows ? Service() : null;

View File

@@ -1,10 +1,5 @@
extension StringExtension on String { extension StringExtension on String {
bool get isUrl { bool get isUrl {
try { return RegExp(r'^(http|https|ftp)://').hasMatch(this);
Uri.parse(this);
return true;
} catch (e) {
return false;
}
} }
} }

View File

@@ -10,5 +10,5 @@ extension TextStyleExtension on TextStyle {
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
TextStyle get toMinus => copyWith(fontSize: fontSize! - 1); TextStyle get toMinus => copyWith(fontSize: fontSize! - 2);
} }

View File

@@ -17,6 +17,7 @@ class AppController {
late ClashConfig clashConfig; late ClashConfig clashConfig;
late Measure measure; late Measure measure;
late Function updateClashConfigDebounce; late Function updateClashConfigDebounce;
late Function addCheckIpNumDebounce;
AppController(this.context) { AppController(this.context) {
appState = context.read<AppState>(); appState = context.read<AppState>();
@@ -25,6 +26,9 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async { updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig(); await updateClashConfig();
}); });
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
measure = Measure.of(context); measure = Measure.of(context);
} }
@@ -66,19 +70,10 @@ class AppController {
updateTraffic() { updateTraffic() {
globalState.updateTraffic( globalState.updateTraffic(
config: config,
appState: appState, appState: appState,
); );
} }
changeProxy() {
globalState.changeProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
addProfile(Profile profile) async { addProfile(Profile profile) async {
config.setProfile(profile); config.setProfile(profile);
if (config.currentProfileId != null) return; if (config.currentProfileId != null) return;
@@ -125,6 +120,14 @@ class AppController {
}); });
} }
Future rawApplyProfile() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
changeProfile(String? value) async { changeProfile(String? value) async {
if (value == config.currentProfileId) return; if (value == config.currentProfileId) return;
config.currentProfileId = value; config.currentProfileId = value;

View File

@@ -56,7 +56,20 @@ enum ProfileType { file, url }
enum ResultType { success, error } enum ResultType { success, error }
enum MessageType { log, tun, delay, process, now, request, run } enum AppMessageType {
log,
delay,
request,
started,
loaded,
}
enum ServiceMessageType {
protect,
process,
started,
loaded,
}
enum FindProcessMode { always, off } enum FindProcessMode { always, off }

View File

@@ -1,7 +1,6 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.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:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class AboutFragment extends StatelessWidget { class AboutFragment extends StatelessWidget {
@@ -49,16 +48,9 @@ class AboutFragment extends StatelessWidget {
appName, appName,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
FutureBuilder<PackageInfo>( Text(
future: appPackage.packageInfoCompleter.future, globalState.packageInfo.version,
builder: (_, package) { style: Theme.of(context).textTheme.labelLarge,
final version = package.data?.version;
if (version == null) return Container();
return Text(
version,
style: Theme.of(context).textTheme.labelLarge,
);
},
) )
], ],
) )

View File

@@ -35,7 +35,6 @@ class _AccessFragmentState extends State<AccessFragment> {
}); });
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
@@ -112,55 +111,48 @@ class _AccessFragmentState extends State<AccessFragment> {
); );
} }
_buildSelectedAllButton({ Widget _buildSelectedAllButton({
required bool isAccessControl, required bool isAccessControl,
required bool isSelectedAll, required bool isSelectedAll,
required List<String> allValueList, required List<String> allValueList,
}) { }) {
WidgetsBinding.instance.addPostFrameCallback((_) { final tooltip = isSelectedAll
final tooltip = isSelectedAll ? appLocalizations.cancelSelectAll
? appLocalizations.cancelSelectAll : appLocalizations.selectAll;
: appLocalizations.selectAll; return AbsorbPointer(
final commonScaffoldState = absorbing: !isAccessControl,
context.findAncestorStateOfType<CommonScaffoldState>(); child: FloatingActionButton(
commonScaffoldState?.floatingActionButton = DisabledMask( tooltip: tooltip,
status: !isAccessControl, onPressed: () {
child: AbsorbPointer( final config = globalState.appController.config;
absorbing: !isAccessControl, final isAccept =
child: FloatingActionButton ( config.accessControl.mode == AccessControlMode.acceptSelected;
tooltip: tooltip,
onPressed: () {
final config = globalState.appController.config;
final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) { if (isSelectedAll) {
config.accessControl = switch (isAccept) { config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith( true => config.accessControl.copyWith(
acceptList: [], acceptList: [],
), ),
false => config.accessControl.copyWith( false => config.accessControl.copyWith(
rejectList: [], rejectList: [],
), ),
}; };
} else { } else {
config.accessControl = switch (isAccept) { config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith( true => config.accessControl.copyWith(
acceptList: allValueList, acceptList: allValueList,
), ),
false => config.accessControl.copyWith( false => config.accessControl.copyWith(
rejectList: allValueList, rejectList: allValueList,
), ),
}; };
} }
}, },
child: isSelectedAll child: isSelectedAll
? const Icon(Icons.deselect) ? const Icon(Icons.deselect)
: const Icon(Icons.select_all), : const Icon(Icons.select_all),
), ),
), );
);
});
} }
Widget _buildPackageList() { Widget _buildPackageList() {
@@ -213,137 +205,141 @@ class _AccessFragmentState extends State<AccessFragment> {
accessControlMode == AccessControlMode.acceptSelected accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc ? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc; : appLocalizations.accessControlNotAllowDesc;
_buildSelectedAllButton(
isAccessControl: isAccessControl,
isSelectedAll: valueList.length == packageNameList.length,
allValueList: packageNameList,
);
return DisabledMask( return DisabledMask(
status: !isAccessControl, status: !isAccessControl,
child: Column( child: FloatLayout(
children: [ floatingWidget: FloatWrapper(
AbsorbPointer( child: _buildSelectedAllButton(
absorbing: !isAccessControl, isAccessControl: isAccessControl,
child: Padding( isSelectedAll: valueList.length == packageNameList.length,
padding: const EdgeInsets.only( allValueList: packageNameList,
top: 4, ),
bottom: 4, ),
left: 16, child: Column(
right: 8, children: [
), AbsorbPointer(
child: Row( absorbing: !isAccessControl,
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Padding(
mainAxisSize: MainAxisSize.max, padding: const EdgeInsets.only(
children: [ top: 4,
Expanded( bottom: 4,
child: IntrinsicHeight( left: 16,
child: Column( right: 8,
mainAxisSize: MainAxisSize.max, ),
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expanded( mainAxisSize: MainAxisSize.max,
child: Row( children: [
children: [ Expanded(
Flexible( child: IntrinsicHeight(
child: Text( child: Column(
appLocalizations.selected, mainAxisSize: MainAxisSize.max,
style: Theme.of(context) crossAxisAlignment: CrossAxisAlignment.start,
.textTheme children: [
.labelLarge Expanded(
?.copyWith( child: Row(
color: Theme.of(context) children: [
.colorScheme Flexible(
.primary, child: Text(
), appLocalizations.selected,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
), ),
), const Flexible(
const Flexible( child: SizedBox(
child: SizedBox( width: 8,
width: 8, ),
), ),
), Flexible(
Flexible( child: Text(
child: Text( "${valueList.length}",
"${valueList.length}", style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .labelLarge
.labelLarge ?.copyWith(
?.copyWith( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .primary,
.primary, ),
), ),
), ),
), ],
], ),
), ),
), Flexible(
Flexible( child: Text(describe),
child: Text(describe), )
) ],
], ),
), ),
), ),
), Row(
Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, children: [
children: [ Flexible(
Flexible( child: _buildSearchButton(currentPackages)),
child: _buildSearchButton(currentPackages)), Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildFilterSystemAppButton()), Flexible(child: _buildAppProxyModePopup()),
Flexible(child: _buildAppProxyModePopup()), ],
], ),
), ],
], ),
), ),
), ),
), Expanded(
Expanded( flex: 1,
flex: 1, child: FadeBox(
child: FadeBox( key: const Key("fade_box"),
key: const Key("fade_box"), child: currentPackages.isEmpty
child: currentPackages.isEmpty ? const Center(
? const Center( child: CircularProgressIndicator(),
child: CircularProgressIndicator(), )
) : ListView.builder(
: ListView.builder( itemCount: currentPackages.length,
itemCount: currentPackages.length, itemBuilder: (_, index) {
itemBuilder: (_, index) { final package = currentPackages[index];
final package = currentPackages[index]; return PackageListItem(
return PackageListItem( key: Key(package.packageName),
key: Key(package.packageName), package: package,
package: package, value:
value: valueList.contains(package.packageName),
valueList.contains(package.packageName), isActive: isAccessControl,
isActive: isAccessControl, onChanged: (value) {
onChanged: (value) { if (value == true) {
if (value == true) { valueList.add(package.packageName);
valueList.add(package.packageName); } else {
} else { valueList.remove(package.packageName);
valueList.remove(package.packageName); }
} final config =
final config = globalState.appController.config;
globalState.appController.config; if (accessControlMode ==
if (accessControlMode == AccessControlMode.acceptSelected) {
AccessControlMode.acceptSelected) { config.accessControl =
config.accessControl = config.accessControl.copyWith(
config.accessControl.copyWith( acceptList: valueList,
acceptList: valueList, );
); } else {
} else { config.accessControl =
config.accessControl = config.accessControl.copyWith(
config.accessControl.copyWith( rejectList: valueList,
rejectList: valueList, );
); }
} },
}, );
); },
}, ),
), ),
), ),
), ],
], ),
), ),
); );
}, },

View File

@@ -89,6 +89,24 @@ class ApplicationSettingFragment extends StatelessWidget {
); );
}, },
), ),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.isExclude,
builder: (_, isExclude, child) {
return ListItem.switchItem(
leading: const Icon(Icons.visibility_off),
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: isExclude,
onChanged: (value) {
final config = context.read<Config>();
config.isExclude = value;
},
),
);
},
),
if (Platform.isAndroid) if (Platform.isAndroid)
Selector<Config, bool>( Selector<Config, bool>(
selector: (_, config) => config.isAnimateToPage, selector: (_, config) => config.isAnimateToPage,

View File

@@ -6,7 +6,6 @@ import 'package:fl_clash/models/dav.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/section.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:provider/provider.dart';
@@ -34,9 +33,9 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
final res = await commonScaffoldState?.loadingRun<bool>(() async { final res = await commonScaffoldState?.loadingRun<bool>(() async {
return await _client?.backup(); return await _client?.backup();
}); });
if(res != true) return; if (res != true) return;
globalState.showMessage( globalState.showMessage(
title: appLocalizations.recovery, title: appLocalizations.backup,
message: TextSpan(text: appLocalizations.backupSuccess), message: TextSpan(text: appLocalizations.backupSuccess),
); );
} }
@@ -46,7 +45,7 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
final res = await commonScaffoldState?.loadingRun<bool>(() async { final res = await commonScaffoldState?.loadingRun<bool>(() async {
return await _client?.recovery(recoveryOption: recoveryOption); return await _client?.recovery(recoveryOption: recoveryOption);
}); });
if(res != true) return; if (res != true) return;
globalState.showMessage( globalState.showMessage(
title: appLocalizations.recovery, title: appLocalizations.recovery,
message: TextSpan(text: appLocalizations.recoverySuccess), message: TextSpan(text: appLocalizations.recoverySuccess),
@@ -69,26 +68,22 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
if (dav == null) { if (dav == null) {
return ListView( return ListView(
children: [ children: [
Section( ListHeader(
title: appLocalizations.account, title: appLocalizations.account,
child: Builder( ),
builder: (_) { ListItem(
return ListItem( leading: const Icon(Icons.account_box),
leading: const Icon(Icons.account_box), title: Text(appLocalizations.noInfo),
title: Text(appLocalizations.noInfo), subtitle: Text(appLocalizations.pleaseBindWebDAV),
subtitle: Text(appLocalizations.pleaseBindWebDAV), trailing: FilledButton.tonal(
trailing: FilledButton.tonal( onPressed: () {
onPressed: () { _showAddWebDAV(dav);
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
);
}, },
child: Text(
appLocalizations.bind,
),
), ),
) ),
], ],
); );
} }
@@ -96,62 +91,60 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
final pingFuture = _client!.pingCompleter.future; final pingFuture = _client!.pingCompleter.future;
return ListView( return ListView(
children: [ children: [
Section( ListHeader(title: appLocalizations.account),
title: appLocalizations.account, ListItem(
child: ListItem( leading: const Icon(Icons.account_box),
leading: const Icon(Icons.account_box), title: TooltipText(
title: TooltipText( text: Text(
text: Text( dav.user,
dav.user, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis,
),
), ),
subtitle: Padding( ),
padding: const EdgeInsets.symmetric(vertical: 4), subtitle: Padding(
child: Row( padding: const EdgeInsets.symmetric(vertical: 4),
crossAxisAlignment: CrossAxisAlignment.center, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.center,
Text(appLocalizations.connectivity), children: [
FutureBuilder<bool>( Text(appLocalizations.connectivity),
future: pingFuture, FutureBuilder<bool>(
builder: (_, snapshot) { future: pingFuture,
return Center( builder: (_, snapshot) {
child: FadeBox( return Center(
key: const Key("fade_box_1"), child: FadeBox(
child: snapshot.connectionState == key: const Key("fade_box_1"),
ConnectionState.waiting child: snapshot.connectionState ==
? const SizedBox( ConnectionState.waiting
width: 12, ? const SizedBox(
height: 12, width: 12,
child: CircularProgressIndicator( height: 12,
strokeWidth: 1, child: CircularProgressIndicator(
), strokeWidth: 1,
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
), ),
), )
); : Container(
}, decoration: BoxDecoration(
), shape: BoxShape.circle,
], color: snapshot.data == true
), ? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
), ),
trailing: FilledButton.tonal( ),
onPressed: () { trailing: FilledButton.tonal(
_showAddWebDAV(dav); onPressed: () {
}, _showAddWebDAV(dav);
child: Text( },
appLocalizations.edit, child: Text(
), appLocalizations.edit,
), ),
), ),
), ),
@@ -161,22 +154,21 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
return FadeBox( return FadeBox(
key: const Key("fade_box_2"), key: const Key("fade_box_2"),
child: snapshot.data == true child: snapshot.data == true
? Section( ? Column(
title: appLocalizations.backupAndRecovery, children: [
child: Column( ListHeader(
children: [ title: appLocalizations.backupAndRecovery),
ListItem( ListItem(
onTab: _backup, onTab: _backup,
title: Text(appLocalizations.backup), title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.backupDesc), subtitle: Text(appLocalizations.backupDesc),
), ),
ListItem( ListItem(
onTab: _handleRecovery, onTab: _handleRecovery,
title: Text(appLocalizations.recovery), title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.recoveryDesc), subtitle: Text(appLocalizations.recoveryDesc),
), ),
], ],
),
) )
: Container(), : Container(),
); );
@@ -228,7 +220,6 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
Navigator.pop(context); Navigator.pop(context);
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();

View File

@@ -39,81 +39,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
} }
} }
Widget _buildAppSection() {
final items = [
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.systemProxy = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand_outlined),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
delegate: SwitchDelegate(
value: isCompatible,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.updateClashConfig(isPatch: false);
await appController.updateGroups();
appController.changeProxy();
},
),
);
},
),
];
return Section(
title: appLocalizations.app,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
_showLogLevelDialog(LogLevel value) { _showLogLevelDialog(LogLevel value) {
globalState.showCommonDialog( globalState.showCommonDialog(
child: AlertDialog( child: AlertDialog(
@@ -150,238 +75,364 @@ class _ConfigFragmentState extends State<ConfigFragment> {
); );
} }
Widget _buildGeneralSection() { _showUaDialog(String? value) {
final items = [ const uas = [
Selector<ClashConfig, LogLevel>( null,
selector: (_, clashConfig) => clashConfig.logLevel, "clash-verge/v1.6.6",
builder: (_, value, __) { "ClashforWindows/0.19.23",
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTab: () {
_showLogLevelDialog(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTab: () {
_modifyMixedPort(mixedPort);
},
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
},
child: Text(
"$mixedPort",
),
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
appController.updateClashConfigDebounce();
},
),
);
},
),
]; ];
return Section( globalState.showCommonDialog(
title: appLocalizations.general, child: AlertDialog(
child: Column( title: const Text("UA"),
children: [ contentPadding: const EdgeInsets.symmetric(
for (final item in items) ...[ horizontal: 8,
item, vertical: 16,
if (items.last != item) ),
const Divider( content: SizedBox(
height: 0, width: 250,
) child: Wrap(
] children: [
], for (final ua in uas)
ListItem.radio(
delegate: RadioDelegate<String?>(
value: ua,
groupValue: value,
onChanged: (String? value) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = value;
appController.updateClashConfigDebounce();
Navigator.of(context).pop();
},
),
title: Text(ua ?? appLocalizations.defaultText),
)
],
),
),
), ),
); );
} }
Widget _buildMoreSection() { _modifyTestUrl(String testUrl) async {
final items = [ final newTestUrl = await globalState.showCommonDialog<String>(
if (false) child: TestUrlFormDialog(
Selector<ClashConfig, bool>( testUrl: testUrl,
selector: (_, clashConfig) => clashConfig.tun.enable, ),
builder: (_, tunEnable, __) { );
if (newTestUrl != null && newTestUrl != testUrl && mounted) {
try {
if (!newTestUrl.isUrl) {
throw "Invalid url";
}
globalState.appController.config.testUrl = newTestUrl;
globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
List<Widget> _buildAppSection() {
return generateSection(
title: appLocalizations.app,
items: [
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.allowBypass = value;
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.systemProxy = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon( leading: const Icon(Icons.expand_outlined),
Icons.important_devices_outlined title: Text(appLocalizations.compatible),
), subtitle: Text(appLocalizations.compatibleDesc),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: tunEnable, value: isCompatible,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.applyProfile();
},
),
);
},
),
],
);
}
List<Widget> _buildGeneralSection() {
return generateSection(
title: appLocalizations.general,
items: [
Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTab: () {
_showLogLevelDialog(value);
},
);
},
),
Selector<ClashConfig, String?>(
selector: (_, clashConfig) => clashConfig.globalRealUa,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
onTab: () {
_showUaDialog(value);
},
);
},
),
Selector<Config, String>(
selector: (_, config) => config.testUrl,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
onTab: () {
_modifyTestUrl(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTab: () {
_modifyMixedPort(mixedPort);
},
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
},
child: Text(
"$mixedPort",
),
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>(); final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value); clashConfig.allowLan = value;
globalState.appController.updateClashConfigDebounce(); globalState.appController.updateClashConfigDebounce();
}, },
), ),
); );
}, },
), ),
]; Selector<ClashConfig, bool>(
if(items.isEmpty) return Container(); selector: (_, clashConfig) => clashConfig.unifiedDelay,
return Section( builder: (_, unifiedDelay, __) {
title: appLocalizations.general, return ListItem.switchItem(
child: Column( leading: const Icon(Icons.compress_outlined),
children: [ title: Text(appLocalizations.unifiedDelay),
for (final item in items) ...[ subtitle: Text(appLocalizations.unifiedDelayDesc),
item, delegate: SwitchDelegate(
if (items.last != item) value: unifiedDelay,
const Divider( onChanged: (bool value) async {
height: 0, final appController = globalState.appController;
) appController.clashConfig.unifiedDelay = value;
] appController.updateClashConfigDebounce();
], },
), ),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
appController.updateClashConfigDebounce();
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
appController.updateClashConfigDebounce();
},
),
);
},
),
],
);
}
List<Widget> _buildMoreSection() {
return generateSection(
title: appLocalizations.more,
items: [
if (system.isDesktop)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
return ListItem.switchItem(
leading: const Icon(Icons.important_devices_outlined),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: tunEnable,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
globalState.appController.updateClashConfigDebounce();
},
),
);
},
),
],
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> items = [ List<Widget> items = [
_buildAppSection(), ..._buildAppSection(),
_buildGeneralSection(), ..._buildGeneralSection(),
_buildMoreSection(), ..._buildMoreSection(),
]; ];
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.only(bottom: 32), padding: const EdgeInsets.only(bottom: 32),
@@ -414,7 +465,7 @@ class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
portController = TextEditingController(text: "${widget.mixedPort}"); portController = TextEditingController(text: "${widget.mixedPort}");
} }
_handleAddProfileFormURL() async { _handleUpdate() async {
final port = portController.value.text; final port = portController.value.text;
if (port.isEmpty) return; if (port.isEmpty) return;
Navigator.of(context).pop<String>(port); Navigator.of(context).pop<String>(port);
@@ -440,7 +491,64 @@ class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: _handleAddProfileFormURL, onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}
class TestUrlFormDialog extends StatefulWidget {
final String testUrl;
const TestUrlFormDialog({
super.key,
required this.testUrl,
});
@override
State<TestUrlFormDialog> createState() => _TestUrlFormDialogState();
}
class _TestUrlFormDialogState extends State<TestUrlFormDialog> {
late TextEditingController testUrlController;
@override
void initState() {
super.initState();
testUrlController = TextEditingController(text: widget.testUrl);
}
_handleUpdate() async {
final testUrl = testUrlController.value.text;
if (testUrl.isEmpty) return;
Navigator.of(context).pop<String>(testUrl);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.testUrl),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
maxLines: 5,
minLines: 1,
controller: testUrlController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit), child: Text(appLocalizations.submit),
) )
], ],

View File

@@ -139,8 +139,8 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
vertical: 16, vertical: 16,
), ),
child: Wrap( child: Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final keyword in state.keywords) for (final keyword in state.keywords)
CommonChip( CommonChip(
@@ -219,6 +219,10 @@ class ConnectionItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListItem( return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight, tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid leading: Platform.isAndroid
? Container( ? Container(
@@ -249,17 +253,17 @@ class ConnectionItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox( const SizedBox(
height: 12, height: 8,
), ),
Text( Text(
_getSourceText(connection), _getSourceText(connection),
), ),
const SizedBox( const SizedBox(
height: 12, height: 8,
), ),
Wrap( Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final chain in connection.chains) for (final chain in connection.chains)
CommonChip( CommonChip(
@@ -271,9 +275,6 @@ class ConnectionItem extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(
height: 12,
),
], ],
), ),
trailing: IconButton( trailing: IconButton(
@@ -394,8 +395,8 @@ class ConnectionsSearchDelegate extends SearchDelegate {
vertical: 16, vertical: 16,
), ),
child: Wrap( child: Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final keyword in state.keywords) for (final keyword in state.keywords)
CommonChip( CommonChip(

View File

@@ -53,7 +53,7 @@ class _IntranetIPState extends State<IntranetIP> {
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0), padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 24 - 1, height: globalState.appController.measure.titleLargeHeight + 24 - 2,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: ipNotifier, valueListenable: ipNotifier,
builder: (_, value, __) { builder: (_, value, __) {

View File

@@ -52,6 +52,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
isInit: appState.isInit, isInit: appState.isInit,
selectedMap: appState.selectedMap, selectedMap: appState.selectedMap,
isStart: appState.isStart, isStart: appState.isStart,
checkIpNum: appState.checkIpNum,
); );
}, },
builder: (_, state, __) { builder: (_, state, __) {
@@ -137,7 +138,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Container( Container(
height: globalState.appController.measure.titleLargeHeight + height: globalState.appController.measure.titleLargeHeight +
24 - 24 -
1, 2,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0), padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox( child: FadeBox(

View File

@@ -13,18 +13,10 @@ class OutboundMode extends StatelessWidget {
_changeMode(BuildContext context, Mode? value) async { _changeMode(BuildContext context, Mode? value) async {
final appController = globalState.appController; final appController = globalState.appController;
final clashConfig = appController.clashConfig; final clashConfig = appController.clashConfig;
final config = appController.config;
if (value == null || clashConfig.mode == value) return; if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value; clashConfig.mode = value;
await appController.updateClashConfig(); await appController.updateClashConfig();
if (!config.isCompatible) { appController.addCheckIpNumDebounce();
final proxySelected = config.currentSelectedMap[GroupName.Proxy.name];
final globalSelected = config.currentSelectedMap[GroupName.GLOBAL.name];
if (proxySelected != null && globalSelected == null) {
config.updateCurrentSelectedMap(GroupName.GLOBAL.name, proxySelected);
}
}
appController.changeProxy();
} }
@override @override
@@ -64,11 +56,8 @@ class OutboundMode extends StatelessWidget {
), ),
title: Text( title: Text(
Intl.message(item.name), Intl.message(item.name),
style: Theme style:
.of(context) Theme.of(context).textTheme.titleMedium?.toSoftBold,
.textTheme
.titleMedium
?.toSoftBold,
), ),
), ),
], ],

View File

@@ -269,8 +269,8 @@ class LogsSearchDelegate extends SearchDelegate {
vertical: 16, vertical: 16,
), ),
child: Wrap( child: Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final keyword in state.keywords) for (final keyword in state.keywords)
CommonChip( CommonChip(
@@ -328,26 +328,23 @@ class _LogItemState extends State<LogItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final log = widget.log; final log = widget.log;
return ListTile( return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: SelectableText(log.payload ?? ''), title: SelectableText(log.payload ?? ''),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( SelectableText(
padding: const EdgeInsets.only( "${log.dateTime}",
top: 8, style: context.textTheme.bodySmall
), ?.copyWith(color: context.colorScheme.primary),
child: SelectableText(
"${log.dateTime}",
style: context.textTheme.bodySmall
?.copyWith(color: context.colorScheme.primary),
),
), ),
const SizedBox(height: 8,),
Container( Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: CommonChip( child: CommonChip(
onPressed: () { onPressed: () {
if (widget.onClick == null) return; if (widget.onClick == null) return;

View File

@@ -28,6 +28,7 @@ class ProfilesFragment extends StatefulWidget {
class _ProfilesFragmentState extends State<ProfilesFragment> { class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false); final hasPadding = ValueNotifier<bool>(false);
Function? applyConfigDebounce;
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = []; List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
@@ -75,97 +76,116 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
width: 8, width: 8,
) )
]; ];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
}, },
); );
} }
@override
void initState() {
super.initState();
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
hasPadding.dispose(); hasPadding.dispose();
} }
_changeProfile(String? id) async {
final appController = globalState.appController;
final config = appController.config;
if (id == config.currentProfileId) return;
config.currentProfileId = id;
applyConfigDebounce ??= debounce<Function()>(() async {
await appController.applyProfile();
appController.appState.delayMap = {};
appController.saveConfigPreferences();
});
applyConfigDebounce!();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool>( return FloatLayout(
selector: (_, appState) => appState.currentLabel == 'profiles', floatingWidget: FloatWrapper(
builder: (_, isCurrent, child) { child: FloatingActionButton(
if (isCurrent) { heroTag: null,
_initScaffoldState(); onPressed: _handleShowAddExtendPage,
} child: const Icon(
return child!; Icons.add,
}, ),
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
), ),
builder: (context, state, child) { ),
if (state.profiles.isEmpty) { child: Selector<AppState, bool>(
return NullStatus( selector: (_, appState) => appState.currentLabel == 'profiles',
label: appLocalizations.nullProfileDesc, builder: (_, isCurrent, child) {
); if (isCurrent) {
_initScaffoldState();
} }
profileItemKeys = state.profiles return child!;
.map((profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
final isMobile = state.viewMode == ViewMode.mobile;
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
});
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: !isMobile
? EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 56 : 0),
)
: EdgeInsets.only(
bottom: 0 + (hasPadding ? 56 : 0),
),
child: Grid(
mainAxisSpacing: isMobile ? 8 : 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged:
globalState.appController.changeProfile,
),
),
],
),
);
},
),
),
);
}, },
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
profileItemKeys = state.profiles
.map(
(profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
},
);
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 56 : 0),
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: _changeProfile,
),
),
],
),
);
},
),
),
);
},
),
), ),
); );
} }
@@ -201,11 +221,18 @@ class _ProfileItemState extends State<ProfileItem> {
Future updateProfile([isSingle = true]) async { Future updateProfile([isSingle = true]) async {
isUpdating.value = true; isUpdating.value = true;
try { try {
await globalState.appController.updateProfile(widget.profile); final appController = globalState.appController;
await appController.updateProfile(widget.profile);
if (widget.profile.id == appController.config.currentProfile?.id &&
!appController.appState.isStart) {
globalState.appController.rawApplyProfile();
}
} catch (e) { } catch (e) {
isUpdating.value = false; isUpdating.value = false;
if (!isSingle) { if (!isSingle) {
return e.toString(); return e.toString();
} else {
rethrow;
} }
} }
isUpdating.value = false; isUpdating.value = false;
@@ -393,16 +420,7 @@ class _ProfileItemState extends State<ProfileItem> {
final profile = widget.profile; final profile = widget.profile;
final groupValue = widget.groupValue; final groupValue = widget.groupValue;
final onChanged = widget.onChanged; final onChanged = widget.onChanged;
return Selector<AppState, ViewMode>( return CommonCard(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, child) {
if (viewMode == ViewMode.mobile) {
return child!;
}
return CommonCard(
child: child!,
);
},
child: ListItem.radio( child: ListItem.radio(
key: Key(profile.id), key: Key(profile.id),
horizontalTitleGap: 16, horizontalTitleGap: 16,

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@@ -280,7 +279,6 @@ class ProxyGroupView extends StatefulWidget {
class _ProxyGroupViewState extends State<ProxyGroupView> { class _ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false; var isLock = false;
final isBoundaryNotifier = ValueNotifier<bool>(false);
final scrollController = ScrollController(); final scrollController = ScrollController();
var isEnd = false; var isEnd = false;
@@ -374,53 +372,6 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
); );
} }
Widget _androidExpansionHandle(Widget child) {
// return NotificationListener<ScrollNotification>(
// onNotification: (ScrollNotification notification) {
// if (notification is ScrollEndNotification) {
// if (notification.metrics.atEdge) {
// isEnd = notification.metrics.pixels ==
// notification.metrics.maxScrollExtent;
// isBoundaryNotifier.value = true;
// }
// }
// return false;
// },
// child: Listener(
// onPointerMove: (details) {
// double yOffset = details.delta.dy;
// final isEnd = scrollController.position.maxScrollExtent == scrollController.position.pixels;
// final isTop = scrollController.position.minScrollExtent == scrollController.position.pixels;
// if(isEnd || isTop){
// isBoundaryNotifier.value = true;
// } else if (yOffset > 0 && scrollController.position.maxScrollExtent == scrollController.position.pixels) {
// isBoundaryNotifier.value = false;
// } else if (yOffset < 0 && !isEnd) {
// isBoundaryNotifier.value = false;
// }
// },
// child: child,
// ),
// );
return Listener(
onPointerMove: (details) {
double yOffset = details.delta.dy;
final isEnd = scrollController.position.maxScrollExtent ==
scrollController.position.pixels;
final isTop = scrollController.position.minScrollExtent ==
scrollController.position.pixels;
if (isEnd && yOffset < 0) {
isBoundaryNotifier.value = true;
} else if (isTop && yOffset > 0) {
isBoundaryNotifier.value = true;
} else {
isBoundaryNotifier.value = false;
}
},
child: child,
);
}
Widget _buildExpansionGroupView({ Widget _buildExpansionGroupView({
required List<Proxy> proxies, required List<Proxy> proxies,
required int columns, required int columns,
@@ -436,7 +387,6 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
final lines = (sortedProxies.length / columns).ceil(); final lines = (sortedProxies.length / columns).ceil();
final minLines = final minLines =
innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3; innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3;
final hasScrollable = lines > minLines;
final height = (itemHeight + 8) * min(lines, minLines) - 8; final height = (itemHeight + 8) * min(lines, minLines) - 8;
return Selector<Config, Set<String>>( return Selector<Config, Set<String>>(
selector: (_, config) => config.currentUnfoldSet, selector: (_, config) => config.currentUnfoldSet,
@@ -544,75 +494,32 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
children: [ children: [
SizedBox( SizedBox(
height: height, height: height,
child: Platform.isAndroid child: GridView.builder(
? _androidExpansionHandle( key: widget.key,
ValueListenableBuilder( controller: scrollController,
valueListenable: isBoundaryNotifier, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
builder: (_, isBoundary, child) { crossAxisCount: columns,
return Scrollbar( mainAxisSpacing: 8,
thickness: 6, crossAxisSpacing: 8,
interactive: true, mainAxisExtent: _getItemHeight(proxyCardType),
radius: const Radius.circular(6), ),
child: GridView.builder( itemCount: sortedProxies.length,
key: widget.key, itemBuilder: (_, index) {
controller: scrollController, final proxy = sortedProxies[index];
physics: isBoundary || !hasScrollable return _currentProxyNameBuilder(
? const NeverScrollableScrollPhysics() builder: (value) {
: const AlwaysScrollableScrollPhysics(), return ProxyCard(
gridDelegate: style: CommonCardType.filled,
SliverGridDelegateWithFixedCrossAxisCount( type: proxyCardType,
crossAxisCount: columns, isSelected: value == proxy.name,
mainAxisSpacing: 8, key: ValueKey('$groupName.${proxy.name}'),
crossAxisSpacing: 8, proxy: proxy,
mainAxisExtent: _getItemHeight(proxyCardType), groupName: groupName,
), );
itemCount: sortedProxies.length, },
itemBuilder: (_, index) { );
final proxy = sortedProxies[index]; },
return _currentProxyNameBuilder( ),
builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
});
},
),
);
},
),
)
: GridView.builder(
key: widget.key,
controller: scrollController,
physics: !hasScrollable
? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
});
},
),
), ),
], ],
), ),
@@ -624,7 +531,6 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
isBoundaryNotifier.dispose();
scrollController.dispose(); scrollController.dispose();
} }
@@ -831,7 +737,12 @@ class ProxyCard extends StatelessWidget {
groupName, groupName,
proxy.name, proxy.name,
); );
appController.changeProxy(); clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxy.name,
),
);
} }
@override @override

View File

@@ -137,8 +137,8 @@ class _RequestsFragmentState extends State<RequestsFragment> {
vertical: 16, vertical: 16,
), ),
child: Wrap( child: Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final keyword in state.keywords) for (final keyword in state.keywords)
CommonChip( CommonChip(
@@ -214,6 +214,10 @@ class RequestItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListItem( return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight, tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid leading: Platform.isAndroid
? Container( ? Container(
@@ -244,17 +248,17 @@ class RequestItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox( const SizedBox(
height: 12, height: 8,
), ),
Text( Text(
_getSourceText(connection), _getSourceText(connection),
), ),
const SizedBox( const SizedBox(
height: 12, height: 8,
), ),
Wrap( Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final chain in connection.chains) for (final chain in connection.chains)
CommonChip( CommonChip(
@@ -266,9 +270,6 @@ class RequestItem extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(
height: 12,
),
], ],
), ),
); );
@@ -375,8 +376,8 @@ class RequestsSearchDelegate extends SearchDelegate {
vertical: 16, vertical: 16,
), ),
child: Wrap( child: Wrap(
runSpacing: 8, runSpacing: 6,
spacing: 8, spacing: 6,
children: [ children: [
for (final keyword in state.keywords) for (final keyword in state.keywords)
CommonChip( CommonChip(

View File

@@ -2,22 +2,37 @@ 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';
import 'package:fl_clash/models/ffi.dart'; import 'package:fl_clash/models/models.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:path/path.dart' hide context; import 'package:path/path.dart' hide context;
import 'package:provider/provider.dart';
@immutable @immutable
class GeoItem { class GeoItem {
final String label; final String label;
final String key;
final String fileName; final String fileName;
const GeoItem({ const GeoItem({
required this.label, required this.label,
required this.key,
required this.fileName, required this.fileName,
}); });
} }
@immutable
class FileInfo {
final String size;
final DateTime lastModified;
const FileInfo({
required this.size,
required this.lastModified,
});
}
class Resources extends StatefulWidget { class Resources extends StatefulWidget {
const Resources({super.key}); const Resources({super.key});
@@ -26,147 +41,473 @@ class Resources extends StatefulWidget {
} }
class _ResourcesState extends State<Resources> { class _ResourcesState extends State<Resources> {
_updateExternalProvider( List<ExternalProvider> externalProviders = [];
String providerName,
String providerType, List<GlobalObjectKey<_ProviderItemState>> providerItemKeys = [];
) async {
final commonScaffoldState = context.commonScaffoldState; @override
await commonScaffoldState?.loadingRun(() async { void initState() {
final message = await clashCore.updateExternalProvider( super.initState();
providerName: providerName, WidgetsBinding.instance.addPostFrameCallback((_) {
providerType: providerType, _syncExternalProviders();
);
if (message.isNotEmpty) throw message;
}); });
setState(() {});
} }
Future<DateTime> _getGeoFileLastModified(String fileName) async { _syncExternalProviders() async {
final homePath = await appPath.getHomeDirPath(); externalProviders = await clashCore.getExternalProviders();
return await File(join(homePath, fileName)).lastModified(); if (mounted) {
setState(() {});
}
} }
Widget _buildExternalProviderSection() { _updateProviders() async {
return FutureBuilder<List<ExternalProvider>>( final updateProviders = providerItemKeys.map<Future>(
future: () async { (key) async => await key.currentState?.updateProvider(false),
await Future.delayed(const Duration(milliseconds: 200));
return await clashCore.getExternalProviders();
}(),
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("external_providers"),
child: snapshot.data == null || snapshot.data!.isEmpty
? Container()
: Section(
title: appLocalizations.externalResources,
child: Column(
children: [
for (final externalProvider in snapshot.data!)
ListItem(
title: Text(externalProvider.name),
subtitle: Text(
"${externalProvider.type} (${externalProvider.vehicleType})",
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
externalProvider.updateAt.lastUpdateTimeDesc,
style: context.textTheme.bodyMedium,
),
const Padding(
padding: EdgeInsets.only(left: 12,right: 4),
child: VerticalDivider(
endIndent: 6,
width: 4,
indent: 6,
),
),
externalProvider.vehicleType == "HTTP"
? IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
externalProvider.name,
externalProvider.type,
);
},
)
: Container(),
],
),
)
],
),
),
),
);
},
); );
await Future.wait(updateProviders);
_syncExternalProviders();
} }
Widget _buildGeoDataSection() { List<Widget> _buildExternalProviderSection() {
List<GlobalObjectKey<_ProviderItemState>> keys = [];
final res = generateInfoSection(
info: Info(
iconData: Icons.source,
label: appLocalizations.externalResources,
),
actions: [
IconButton.filledTonal(
onPressed: () {
_updateProviders();
},
padding: const EdgeInsets.all(4),
iconSize: 20,
icon: const Icon(
Icons.sync,
),
)
],
items: externalProviders.map(
(externalProvider) {
final key =
GlobalObjectKey<_ProviderItemState>(externalProvider.name);
keys.add(key);
return ProviderItem(
key: key,
provider: externalProvider,
onUpdated: () {
_syncExternalProviders();
},
);
},
),
);
providerItemKeys = keys;
return res;
}
List<Widget> _buildGeoDataSection() {
const geoItems = <GeoItem>[ const geoItems = <GeoItem>[
GeoItem(label: "GeoIp", fileName: mmdbFileName), GeoItem(
GeoItem(label: "GeoSite", fileName: geoSiteFileName), label: "GeoIp",
GeoItem(label: "ASN", fileName: asnFileName), fileName: geoIpFileName,
key: "geoip",
),
GeoItem(label: "GeoSite", fileName: geoSiteFileName, key: "geosite"),
GeoItem(
label: "MMDB",
fileName: mmdbFileName,
key: "mmdb",
),
GeoItem(label: "ASN", fileName: asnFileName, key: "asn"),
]; ];
return Section(
title: appLocalizations.geoData, return generateInfoSection(
child: Column( info: Info(
children: [ iconData: Icons.storage,
for (final geoItem in geoItems) label: appLocalizations.geoData,
ListItem( ),
title: Text(geoItem.label), items: geoItems.map(
subtitle: FutureBuilder<DateTime>( (geoItem) => GeoDataListItem(
future: () async { geoItem: geoItem,
await Future.delayed(const Duration(milliseconds: 200)); ),
return await _getGeoFileLastModified(geoItem.fileName);
}(),
builder: (_, snapshot) {
return Container(
alignment: Alignment.centerLeft,
height: 24,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
snapshot.data!.lastUpdateTimeDesc,
),
),
);
},
),
trailing: IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
geoItem.fileName,
geoItem.label,
);
},
),
),
],
), ),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return generateListView(
children: [ [
_buildGeoDataSection(), ..._buildGeoDataSection(),
_buildExternalProviderSection(), ..._buildExternalProviderSection(),
], ],
); );
} }
} }
class GeoDataListItem extends StatefulWidget {
final GeoItem geoItem;
const GeoDataListItem({
super.key,
required this.geoItem,
});
@override
State<GeoDataListItem> createState() => _GeoDataListItemState();
}
class _GeoDataListItemState extends State<GeoDataListItem> {
final isUpdating = ValueNotifier<bool>(false);
GeoItem get geoItem => widget.geoItem;
_updateUrl(String url) async {
final newUrl = await globalState.showCommonDialog<String>(
child: UpdateGeoUrlFormDialog(
title: geoItem.label,
url: url,
),
);
if (newUrl != null && newUrl != url && mounted) {
try {
if (!newUrl.isUrl) {
throw "Invalid url";
}
final appController = globalState.appController;
appController.clashConfig.geoXUrl =
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: geoItem.label,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
Future<FileInfo> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
final file = File(join(homePath, fileName));
final lastModified = await file.lastModified();
final size = await file.length();
return FileInfo(
size: TrafficValue(value: size).show,
lastModified: lastModified,
);
}
// _uploadGeoFile(String fileName) async {
// final res = await picker.pickerGeoDataFile();
// if (res == null || res.bytes == null) return;
// final homePath = await appPath.getHomeDirPath();
// final file = File(join(homePath, fileName));
// await file.writeAsBytes(
// res.bytes!,
// flush: true,
// );
// setState(() {});
// }
String _buildFileInfoDesc(FileInfo fileInfo) {
return "${fileInfo.size} · ${fileInfo.lastModified.lastUpdateTimeDesc}";
}
Widget _buildSubtitle(String url) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
FutureBuilder<FileInfo>(
future: _getGeoFileLastModified(geoItem.fileName),
builder: (_, snapshot) {
return SizedBox(
height: 24,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
_buildFileInfoDesc(snapshot.data!),
),
),
);
},
),
Text(
url,
style: context.textTheme.bodyMedium?.toLight,
),
const SizedBox(
height: 8,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [
CommonChip(
avatar: const Icon(Icons.edit),
label: appLocalizations.edit,
onPressed: () {
_updateUrl(url);
},
),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateGeoDataItem();
},
),
],
),
],
);
}
_handleUpdateGeoDataItem() async {
await globalState.safeRun<void>(updateGeoDateItem);
setState(() {});
}
updateGeoDateItem() async {
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: geoItem.fileName,
providerType: geoItem.label,
);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;
rethrow;
}
isUpdating.value = false;
return null;
}
@override
void dispose() {
super.dispose();
isUpdating.dispose();
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(geoItem.label),
subtitle: Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.geoXUrl[geoItem.key]!,
builder: (_, value, __) {
return _buildSubtitle(value);
},
),
trailing: SizedBox(
height: 48,
width: 48,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
);
},
),
),
);
}
}
class ProviderItem extends StatefulWidget {
final ExternalProvider provider;
final Function onUpdated;
const ProviderItem({
super.key,
required this.provider,
required this.onUpdated,
});
@override
State<ProviderItem> createState() => _ProviderItemState();
}
class _ProviderItemState extends State<ProviderItem> {
final isUpdating = ValueNotifier<bool>(false);
ExternalProvider get provider => widget.provider;
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProvider);
widget.onUpdated();
}
updateProvider([isSingle = true]) async {
if (provider.vehicleType != "HTTP") return;
isUpdating.value = true;
try {
final message = await clashCore.updateExternalProvider(
providerName: provider.name,
providerType: provider.type,
);
if (message.isNotEmpty) throw message;
} catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
}
isUpdating.value = false;
return null;
}
String _buildProviderDesc() {
return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
}
@override
void dispose() {
super.dispose();
isUpdating.dispose();
}
Widget _buildSubtitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 4,
),
Text(
_buildProviderDesc(),
),
if (provider.vehicleType == "HTTP") ...[
const SizedBox(
height: 8,
),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateProfile();
},
),
],
],
);
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
title: Text(provider.name),
subtitle: _buildSubtitle(),
trailing: SizedBox(
height: 48,
width: 48,
child: ValueListenableBuilder(
valueListenable: isUpdating,
builder: (_, isUpdating, ___) {
return FadeBox(
child: isUpdating
? const Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(),
)
: const SizedBox(),
);
},
),
),
);
}
}
class UpdateGeoUrlFormDialog extends StatefulWidget {
final String title;
final String url;
const UpdateGeoUrlFormDialog({
super.key,
required this.title,
required this.url,
});
@override
State<UpdateGeoUrlFormDialog> createState() => _UpdateGeoUrlFormDialogState();
}
class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
late TextEditingController urlController;
@override
void initState() {
super.initState();
urlController = TextEditingController(text: widget.url);
}
_handleUpdate() async {
final url = urlController.value.text;
if (url.isEmpty) return;
Navigator.of(context).pop<String>(url);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
maxLines: 5,
minLines: 1,
controller: urlController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}

View File

@@ -57,138 +57,120 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
return Intl.message(locale.toString()); return Intl.message(locale.toString());
} }
Widget _getOtherList() { List<Widget> _getOtherList() {
List<Widget> items = [ return generateSection(
ListItem.open( title: appLocalizations.other,
leading: const Icon(Icons.info), items: [
title: Text(appLocalizations.about), ListItem.open(
delegate: OpenDelegate( leading: const Icon(Icons.info),
title: appLocalizations.about, title: Text(appLocalizations.about),
widget: const AboutFragment(), delegate: OpenDelegate(
title: appLocalizations.about,
widget: const AboutFragment(),
),
), ),
),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final item in items) ...[
item,
if (item != items.last)
const Divider(
height: 0,
),
]
], ],
); );
} }
Widget _getSettingList() { List<Widget> _getSettingList() {
List<Widget> items = [ return generateSection(
Selector<Config, String?>( title: appLocalizations.settings,
selector: (_, config) => config.locale, items: [
builder: (_, localeString, __) { Selector<Config, String?>(
final subTitle = localeString ?? appLocalizations.defaultText; selector: (_, config) => config.locale,
final currentLocale = other.getLocaleForString(localeString); builder: (_, localeString, __) {
return ListTile( final subTitle = localeString ?? appLocalizations.defaultText;
leading: const Icon(Icons.language_outlined), final currentLocale = other.getLocaleForString(localeString);
title: Text(appLocalizations.language), return ListTile(
subtitle: Text(Intl.message(subTitle)), leading: const Icon(Icons.language_outlined),
onTap: () { title: Text(appLocalizations.language),
globalState.showCommonDialog( subtitle: Text(Intl.message(subTitle)),
child: AlertDialog( onTap: () {
title: Text(appLocalizations.language), globalState.showCommonDialog(
contentPadding: const EdgeInsets.symmetric( child: AlertDialog(
horizontal: 8, title: Text(appLocalizations.language),
vertical: 16, contentPadding: const EdgeInsets.symmetric(
), horizontal: 8,
content: SizedBox( vertical: 16,
width: 250, ),
child: Wrap( content: SizedBox(
children: [ width: 250,
for (final locale in [ child: Wrap(
null, children: [
...AppLocalizations.delegate.supportedLocales for (final locale in [
]) null,
ListItem.radio( ...AppLocalizations.delegate.supportedLocales
delegate: RadioDelegate<Locale?>( ])
value: locale, ListItem.radio(
groupValue: currentLocale, delegate: RadioDelegate<Locale?>(
onChanged: (Locale? value) { value: locale,
final config = context.read<Config>(); groupValue: currentLocale,
config.locale = value?.toString(); onChanged: (Locale? value) {
Navigator.of(context).pop(); final config = context.read<Config>();
}, config.locale = value?.toString();
), Navigator.of(context).pop();
title: Text(_getLocaleString(locale)), },
) ),
], title: Text(_getLocaleString(locale)),
)
],
),
), ),
), ),
), );
); },
}, );
); },
},
),
ListItem.open(
leading: const Icon(Icons.style),
title: Text(appLocalizations.theme),
subtitle: Text(appLocalizations.themeDesc),
delegate: OpenDelegate(
title: appLocalizations.theme,
widget: const ThemeFragment(),
extendPageWidth: 360,
), ),
),
ListItem.open(
leading: const Icon(Icons.cloud_sync),
title: Text(appLocalizations.backupAndRecovery),
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
delegate: OpenDelegate(
title: appLocalizations.backupAndRecovery,
widget: const BackupAndRecovery(),
),
),
if (Platform.isAndroid)
ListItem.open( ListItem.open(
leading: const Icon(Icons.view_list), leading: const Icon(Icons.style),
title: Text(appLocalizations.accessControl), title: Text(appLocalizations.theme),
subtitle: Text(appLocalizations.accessControlDesc), subtitle: Text(appLocalizations.themeDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.appAccessControl, title: appLocalizations.theme,
widget: const AccessFragment(), widget: const ThemeFragment(),
extendPageWidth: 360,
), ),
), ),
ListItem.open( ListItem.open(
leading: const Icon(Icons.edit), leading: const Icon(Icons.cloud_sync),
title: Text(appLocalizations.override), title: Text(appLocalizations.backupAndRecovery),
subtitle: Text(appLocalizations.overrideDesc), subtitle: Text(appLocalizations.backupAndRecoveryDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.override, title: appLocalizations.backupAndRecovery,
widget: const ConfigFragment(), widget: const BackupAndRecovery(),
extendPageWidth: 360, ),
), ),
), if (Platform.isAndroid)
ListItem.open( ListItem.open(
leading: const Icon(Icons.settings_applications), leading: const Icon(Icons.view_list),
title: Text(appLocalizations.application), title: Text(appLocalizations.accessControl),
subtitle: Text(appLocalizations.applicationDesc), subtitle: Text(appLocalizations.accessControlDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.application, title: appLocalizations.appAccessControl,
widget: const ApplicationSettingFragment(), widget: const AccessFragment(),
),
),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final item in items) ...[
item,
if (item != items.last)
const Divider(
height: 0,
), ),
] ),
ListItem.open(
leading: const Icon(Icons.edit),
title: Text(appLocalizations.override),
subtitle: Text(appLocalizations.overrideDesc),
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
extendPageWidth: 360,
),
),
ListItem.open(
leading: const Icon(Icons.settings_applications),
title: Text(appLocalizations.application),
subtitle: Text(appLocalizations.applicationDesc),
delegate: OpenDelegate(
title: appLocalizations.application,
widget: const ApplicationSettingFragment(),
),
),
], ],
); );
} }
@@ -216,20 +198,16 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
if (state.navigationItems.isEmpty) { if (state.navigationItems.isEmpty) {
return Container(); return Container();
} }
return Section( return Column(
title: appLocalizations.more, children: [
child: _buildNavigationMenu(state.navigationItems), ListHeader(title: appLocalizations.more),
_buildNavigationMenu(state.navigationItems)
],
); );
}, },
), ),
Section( ..._getSettingList(),
title: appLocalizations.settings, ..._getOtherList(),
child: _getSettingList(),
),
Section(
title: appLocalizations.other,
child: _getOtherList(),
),
]; ];
return ListView.builder( return ListView.builder(
itemCount: items.length, itemCount: items.length,

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config", "overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan", "allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN", "allowLanDesc": "Allow access proxy through the LAN",
"tun": "Tun mode", "tun": "TUN mode",
"tunDesc": "only effective in administrator mode", "tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit", "minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event", "minimizeOnExitDesc": "Modify the default system exit event",
@@ -191,5 +191,9 @@
"view": "View", "view": "View",
"cut": "Cut", "cut": "Cut",
"copy": "Copy", "copy": "Copy",
"paste": "Paste" "paste": "Paste",
"testUrl": "Test url",
"sync": "Sync",
"exclude": "Hidden from recent tasks",
"excludeDesc": "When the app is in the background, the app is hidden from the recent task"
} }

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置", "overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理", "allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理", "allowLanDesc": "允许通过局域网访问代理",
"tun": "Tun模式", "tun": "TUN模式",
"tunDesc": "仅在管理员模式生效", "tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化", "minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件", "minimizeOnExitDesc": "修改系统默认退出事件",
@@ -191,5 +191,9 @@
"view": "查看", "view": "查看",
"cut": "剪切", "cut": "剪切",
"copy": "复制", "copy": "复制",
"paste": "粘贴" "paste": "粘贴",
"testUrl": "测速链接",
"sync": "同步",
"exclude": "从最近任务中隐藏",
"excludeDesc": "应用在后台时,从最近任务中隐藏应用"
} }

View File

@@ -121,6 +121,10 @@ class MessageLookup extends MessageLookupByLibrary {
"download": MessageLookupByLibrary.simpleMessage("Download"), "download": MessageLookupByLibrary.simpleMessage("Download"),
"edit": MessageLookupByLibrary.simpleMessage("Edit"), "edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"), "en": MessageLookupByLibrary.simpleMessage("English"),
"exclude":
MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"),
"excludeDesc": MessageLookupByLibrary.simpleMessage(
"When the app is in the background, the app is hidden from the recent task"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"), "exit": MessageLookupByLibrary.simpleMessage("Exit"),
"expirationTime": "expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"), MessageLookupByLibrary.simpleMessage("Expiration time"),
@@ -260,6 +264,7 @@ class MessageLookup extends MessageLookupByLibrary {
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("Submit"), "submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), "systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage( "systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"), "Attach HTTP proxy to VpnService"),
@@ -269,6 +274,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP concurrent"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP concurrent"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage( "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage(
"Enabling it will allow TCP concurrency"), "Enabling it will allow TCP concurrency"),
"testUrl": MessageLookupByLibrary.simpleMessage("Test url"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"), "theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"), "themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage( "themeDesc": MessageLookupByLibrary.simpleMessage(
@@ -277,7 +283,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("tip"), "tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"), "tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("Tun mode"), "tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tunDesc": MessageLookupByLibrary.simpleMessage( "tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"), "only effective in administrator mode"),
"unableToUpdateCurrentProfileDesc": "unableToUpdateCurrentProfileDesc":

View File

@@ -100,6 +100,9 @@ class MessageLookup extends MessageLookupByLibrary {
"download": MessageLookupByLibrary.simpleMessage("下载"), "download": MessageLookupByLibrary.simpleMessage("下载"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"), "edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"), "en": MessageLookupByLibrary.simpleMessage("英语"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exit": MessageLookupByLibrary.simpleMessage("退出"), "exit": MessageLookupByLibrary.simpleMessage("退出"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
@@ -209,6 +212,7 @@ class MessageLookup extends MessageLookupByLibrary {
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("提交"), "submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc": "systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"), MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
@@ -217,6 +221,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"), MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"theme": MessageLookupByLibrary.simpleMessage("主题"), "theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
@@ -224,7 +229,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("提示"), "tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"), "tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("Tun模式"), "tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"unableToUpdateCurrentProfileDesc": "unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"), MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),

View File

@@ -430,10 +430,10 @@ class AppLocalizations {
); );
} }
/// `Tun mode` /// `TUN mode`
String get tun { String get tun {
return Intl.message( return Intl.message(
'Tun mode', 'TUN mode',
name: 'tun', name: 'tun',
desc: '', desc: '',
args: [], args: [],
@@ -1979,6 +1979,46 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Test url`
String get testUrl {
return Intl.message(
'Test url',
name: 'testUrl',
desc: '',
args: [],
);
}
/// `Sync`
String get sync {
return Intl.message(
'Sync',
name: 'sync',
desc: '',
args: [],
);
}
/// `Hidden from recent tasks`
String get exclude {
return Intl.message(
'Hidden from recent tasks',
name: 'exclude',
desc: '',
args: [],
);
}
/// `When the app is in the background, the app is hidden from the recent task`
String get excludeDesc {
return Intl.message(
'When the app is in the background, the app is hidden from the recent task',
name: 'excludeDesc',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/tile.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:package_info_plus/package_info_plus.dart';
import 'application.dart'; import 'application.dart';
import 'l10n/l10n.dart'; import 'l10n/l10n.dart';
import 'models/models.dart'; import 'models/models.dart';
@@ -15,6 +17,8 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await android?.init(); await android?.init();
await window?.init(); await window?.init();
clashCore.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final appState = AppState( final appState = AppState(
@@ -42,6 +46,7 @@ Future<void> main() async {
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> vpnService() async { Future<void> vpnService() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
globalState.isVpnService = true;
final config = await preferences.getConfig() ?? Config(); final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final appState = AppState( final appState = AppState(
@@ -49,68 +54,112 @@ Future<void> vpnService() async {
isCompatible: config.isCompatible, isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap, selectedMap: config.currentSelectedMap,
); );
clashMessage.addListener(ClashMessageListenerWithVpn(onTun: (String fd) {
proxyManager.setProtect(
int.parse(fd),
);
}));
await globalState.init( await globalState.init(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); );
globalState.applyProfile( proxy?.setServiceMessageHandler(
appState: appState, ServiceMessageHandler(
config: config, onProtect: (Fd fd) async {
clashConfig: clashConfig, await proxy?.setProtect(fd.value);
clashCore.setFdMap(fd.id);
},
onProcess: (Process process) async {
print(process);
var packageName = await app?.resolverProcess(process);
clashCore.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
},
onStarted: (String runTime) {
globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
},
onLoaded: (String groupName) {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
);
},
),
); );
final appLocalizations = await AppLocalizations.load( final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.locale) ?? other.getLocaleForString(config.locale) ??
WidgetsBinding.instance.platformDispatcher.locale, WidgetsBinding.instance.platformDispatcher.locale,
); );
await app?.tip(appLocalizations.startVpn);
handleStart() async { await globalState.startSystemProxy(
await app?.tip(appLocalizations.startVpn); appState: appState,
await globalState.startSystemProxy( config: config,
appState: appState, clashConfig: clashConfig,
config: config, );
clashConfig: clashConfig,
);
globalState.updateTraffic(config: config);
globalState.updateFunctionLists = [
() {
globalState.updateTraffic(config: config);
}
];
}
handleStart();
tile?.addListener( tile?.addListener(
TileListenerWithVpn( TileListenerWithVpn(
onStop: () async { onStop: () async {
await app?.tip(appLocalizations.stopVpn); await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy(); await globalState.stopSystemProxy();
clashCore.shutdown();
exit(0); exit(0);
}, },
), ),
); );
globalState.updateTraffic();
globalState.updateFunctionLists = [
() {
globalState.updateTraffic();
}
];
} }
class ClashMessageListenerWithVpn with ClashMessageListener { @immutable
final Function(String fd) _onTun; class ServiceMessageHandler with ServiceMessageListener {
final Function(Fd fd) _onProtect;
final Function(Process process) _onProcess;
final Function(String runTime) _onStarted;
final Function(String groupName) _onLoaded;
ClashMessageListenerWithVpn({ const ServiceMessageHandler({
required Function(String fd) onTun, required Function(Fd fd) onProtect,
}) : _onTun = onTun; required Function(Process process) onProcess,
required Function(String runTime) onStarted,
required Function(String groupName) onLoaded,
}) : _onProtect = onProtect,
_onProcess = onProcess,
_onStarted = onStarted,
_onLoaded = onLoaded;
@override @override
void onTun(String fd) { onProtect(Fd fd) {
_onTun(fd); _onProtect(fd);
}
@override
onProcess(Process process) {
_onProcess(process);
}
@override
onStarted(String runTime) {
_onStarted(runTime);
}
@override
onLoaded(String groupName) {
_onLoaded(groupName);
} }
} }

View File

@@ -34,6 +34,7 @@ class AppState with ChangeNotifier {
List<Group> _groups; List<Group> _groups;
double _viewWidth; double _viewWidth;
List<Connection> _requests; List<Connection> _requests;
num _checkIpNum;
AppState({ AppState({
required Mode mode, required Mode mode,
@@ -47,6 +48,7 @@ class AppState with ChangeNotifier {
_viewWidth = 0, _viewWidth = 0,
_selectedMap = selectedMap, _selectedMap = selectedMap,
_sortNum = 0, _sortNum = 0,
_checkIpNum = 0,
_requests = [], _requests = [],
_mode = mode, _mode = mode,
_totalTraffic = Traffic(), _totalTraffic = Traffic(),
@@ -123,9 +125,10 @@ class AppState with ChangeNotifier {
final index = groups.indexWhere((element) => element.name == proxyName); final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return proxyName; if (index == -1) return proxyName;
final group = groups[index]; final group = groups[index];
return getRealProxyName(selectedMap.containsKey(proxyName) return getRealProxyName((selectedMap.containsKey(proxyName)
? selectedMap[proxyName] ? selectedMap[proxyName]
: group.now); : group.now)) ??
proxyName;
} }
String? get showProxyName { String? get showProxyName {
@@ -240,6 +243,15 @@ class AppState with ChangeNotifier {
} }
} }
num get checkIpNum => _checkIpNum;
set checkIpNum(num value) {
if (_checkIpNum != value) {
_checkIpNum = value;
notifyListeners();
}
}
Mode get mode => _mode; Mode get mode => _mode;
set mode(Mode value) { set mode(Mode value) {

View File

@@ -1,7 +1,10 @@
// ignore_for_file: invalid_annotation_target // ignore_for_file: invalid_annotation_target
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -104,6 +107,8 @@ class Dns {
} }
} }
typedef GeoXMap = Map<String, String>;
@JsonSerializable() @JsonSerializable()
class ClashConfig extends ChangeNotifier { class ClashConfig extends ChangeNotifier {
int _mixedPort; int _mixedPort;
@@ -118,7 +123,9 @@ class ClashConfig extends ChangeNotifier {
bool _tcpConcurrent; bool _tcpConcurrent;
Tun _tun; Tun _tun;
Dns _dns; Dns _dns;
GeoXMap _geoXUrl;
List<String> _rules; List<String> _rules;
String? _globalRealUa;
ClashConfig() ClashConfig()
: _mixedPort = 7890, : _mixedPort = 7890,
@@ -133,6 +140,7 @@ class ClashConfig extends ChangeNotifier {
_geodataLoader = geodataLoaderMemconservative, _geodataLoader = geodataLoaderMemconservative,
_externalController = '', _externalController = '',
_dns = Dns(), _dns = Dns(),
_geoXUrl = defaultGeoXMap,
_rules = []; _rules = [];
@JsonKey(name: "mixed-port", defaultValue: 7890) @JsonKey(name: "mixed-port", defaultValue: 7890)
@@ -235,7 +243,12 @@ class ClashConfig extends ChangeNotifier {
} }
} }
Tun get tun => _tun; Tun get tun {
if (Platform.isAndroid) {
return _tun.copyWith(enable: false);
}
return _tun;
}
set tun(Tun value) { set tun(Tun value) {
if (_tun != value) { if (_tun != value) {
@@ -262,6 +275,35 @@ class ClashConfig extends ChangeNotifier {
} }
} }
@JsonKey(name: "global-ua", defaultValue: null)
String get globalUa {
if (_globalRealUa == null) {
return globalState.packageInfo.ua;
} else {
return _globalRealUa!;
}
}
@JsonKey(name: "global-real-ua", defaultValue: null)
String? get globalRealUa => _globalRealUa;
set globalRealUa(String? value) {
if (_globalRealUa != value) {
_globalRealUa = value;
notifyListeners();
}
}
@JsonKey(name: "geox-url", defaultValue: defaultGeoXMap)
GeoXMap get geoXUrl => _geoXUrl;
set geoXUrl(GeoXMap value) {
if (!const MapEquality<String, String>().equals(value, _geoXUrl)) {
_geoXUrl = value;
notifyListeners();
}
}
update([ClashConfig? clashConfig]) { update([ClashConfig? clashConfig]) {
if (clashConfig != null) { if (clashConfig != null) {
_mixedPort = clashConfig._mixedPort; _mixedPort = clashConfig._mixedPort;
@@ -271,6 +313,7 @@ class ClashConfig extends ChangeNotifier {
_tun = clashConfig._tun; _tun = clashConfig._tun;
_dns = clashConfig._dns; _dns = clashConfig._dns;
_rules = clashConfig._rules; _rules = clashConfig._rules;
_globalRealUa = clashConfig.globalRealUa;
} }
notifyListeners(); notifyListeners();
} }
@@ -282,9 +325,4 @@ class ClashConfig extends ChangeNotifier {
factory ClashConfig.fromJson(Map<String, dynamic> json) { factory ClashConfig.fromJson(Map<String, dynamic> json) {
return _$ClashConfigFromJson(json); return _$ClashConfigFromJson(json);
} }
@override
String toString() {
return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _mode: $_mode, _logLevel: $_logLevel, _tun: $_tun, _dns: $_dns, _rules: $_rules}';
}
} }

View File

@@ -56,10 +56,12 @@ class Config extends ChangeNotifier {
bool _autoCheckUpdate; bool _autoCheckUpdate;
bool _allowBypass; bool _allowBypass;
bool _systemProxy; bool _systemProxy;
bool _isExclude;
DAV? _dav; DAV? _dav;
ProxiesType _proxiesType; ProxiesType _proxiesType;
ProxyCardType _proxyCardType; ProxyCardType _proxyCardType;
int _proxiesColumns; int _proxiesColumns;
String _testUrl;
Config() Config()
: _profiles = [], : _profiles = [],
@@ -75,9 +77,11 @@ class Config extends ChangeNotifier {
_isAccessControl = false, _isAccessControl = false,
_autoCheckUpdate = true, _autoCheckUpdate = true,
_systemProxy = true, _systemProxy = true,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(), _accessControl = const AccessControl(),
_isAnimateToPage = true, _isAnimateToPage = true,
_allowBypass = true, _allowBypass = true,
_isExclude = false,
_proxyCardType = ProxyCardType.expand, _proxyCardType = ProxyCardType.expand,
_proxiesType = ProxiesType.tab, _proxiesType = ProxiesType.tab,
_proxiesColumns = 2; _proxiesColumns = 2;
@@ -414,6 +418,26 @@ class Config extends ChangeNotifier {
} }
} }
@JsonKey(name: "test-url", defaultValue: defaultTestUrl)
String get testUrl => _testUrl;
set testUrl(String value) {
if (_testUrl != value) {
_testUrl = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get isExclude => _isExclude;
set isExclude(bool value) {
if (_isExclude != value) {
_isExclude = value;
notifyListeners();
}
}
update([ update([
Config? config, Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all, RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -446,6 +470,7 @@ class Config extends ChangeNotifier {
_isAnimateToPage = config._isAnimateToPage; _isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate; _autoCheckUpdate = config._autoCheckUpdate;
_dav = config._dav; _dav = config._dav;
_testUrl = config.testUrl;
} }
notifyListeners(); notifyListeners();
} }

View File

@@ -3,19 +3,32 @@
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart'; import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/connection.dart'; import 'package:fl_clash/models/connection.dart';
import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/ffi.g.dart'; part 'generated/ffi.g.dart';
part 'generated/ffi.freezed.dart'; part 'generated/ffi.freezed.dart';
@freezed
class ConfigExtendedParams with _$ConfigExtendedParams {
const factory ConfigExtendedParams({
@JsonKey(name: "is-patch") required bool isPatch,
@JsonKey(name: "is-compatible") required bool isCompatible,
@JsonKey(name: "selected-map") required SelectedMap selectedMap,
@JsonKey(name: "test-url") required String testUrl,
}) = _ConfigExtendedParams;
factory ConfigExtendedParams.fromJson(Map<String, Object?> json) =>
_$ConfigExtendedParamsFromJson(json);
}
@freezed @freezed
class UpdateConfigParams with _$UpdateConfigParams { class UpdateConfigParams with _$UpdateConfigParams {
const factory UpdateConfigParams({ const factory UpdateConfigParams({
@JsonKey(name: "profile-path") String? profilePath, @JsonKey(name: "profile-path") String? profilePath,
required ClashConfig config, required ClashConfig config,
@JsonKey(name: "is-patch") required bool isPatch, required ConfigExtendedParams params,
@JsonKey(name: "is-compatible") required bool isCompatible,
}) = _UpdateConfigParams; }) = _UpdateConfigParams;
factory UpdateConfigParams.fromJson(Map<String, Object?> json) => factory UpdateConfigParams.fromJson(Map<String, Object?> json) =>
@@ -34,14 +47,25 @@ class ChangeProxyParams with _$ChangeProxyParams {
} }
@freezed @freezed
class Message with _$Message { class AppMessage with _$AppMessage {
const factory Message({ const factory AppMessage({
required MessageType type, required AppMessageType type,
dynamic data, dynamic data,
}) = _Message; }) = _AppMessage;
factory Message.fromJson(Map<String, Object?> json) => factory AppMessage.fromJson(Map<String, Object?> json) =>
_$MessageFromJson(json); _$AppMessageFromJson(json);
}
@freezed
class ServiceMessage with _$ServiceMessage {
const factory ServiceMessage({
required ServiceMessageType type,
dynamic data,
}) = _ServiceMessage;
factory ServiceMessage.fromJson(Map<String, Object?> json) =>
_$ServiceMessageFromJson(json);
} }
@freezed @freezed
@@ -75,6 +99,16 @@ class Process with _$Process {
_$ProcessFromJson(json); _$ProcessFromJson(json);
} }
@freezed
class Fd with _$Fd {
const factory Fd({
required int id,
required int value,
}) = _Fd;
factory Fd.fromJson(Map<String, Object?> json) => _$FdFromJson(json);
}
@freezed @freezed
class ProcessMapItem with _$ProcessMapItem { class ProcessMapItem with _$ProcessMapItem {
const factory ProcessMapItem({ const factory ProcessMapItem({
@@ -98,3 +132,27 @@ class ExternalProvider with _$ExternalProvider {
factory ExternalProvider.fromJson(Map<String, Object?> json) => factory ExternalProvider.fromJson(Map<String, Object?> json) =>
_$ExternalProviderFromJson(json); _$ExternalProviderFromJson(json);
} }
abstract mixin class AppMessageListener {
void onLog(Log log) {}
void onDelay(Delay delay) {}
void onRequest(Connection connection) {}
void onStarted(String runTime) {}
void onLoaded(String groupName) {}
}
abstract mixin class ServiceMessageListener {
onProtect(Fd fd) {}
onProcess(Process process) {}
onStarted(String runTime) {}
onLoaded(String groupName) {}
}

View File

@@ -51,7 +51,21 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false ..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false
..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>) ..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>)
..dns = Dns.fromJson(json['dns'] as Map<String, dynamic>) ..dns = Dns.fromJson(json['dns'] as Map<String, dynamic>)
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList(); ..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList()
..globalRealUa = json['global-real-ua'] as String?
..geoXUrl = (json['geox-url'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
{
'mmdb':
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
'asn':
'https://github.com/xishang0128/geoip/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'
};
Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) => Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -68,6 +82,8 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'tun': instance.tun, 'tun': instance.tun,
'dns': instance.dns, 'dns': instance.dns,
'rules': instance.rules, 'rules': instance.rules,
'global-real-ua': instance.globalRealUa,
'geox-url': instance.geoXUrl,
}; };
const _$ModeEnumMap = { const _$ModeEnumMap = {

View File

@@ -41,7 +41,10 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..proxyCardType = ..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ?? $enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2; ..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
..isExclude = json['isExclude'] as bool? ?? false;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles, 'profiles': instance.profiles,
@@ -66,6 +69,8 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!, 'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!, 'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns, 'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl,
'isExclude': instance.isExclude,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {

View File

@@ -14,6 +14,234 @@ T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError( final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
ConfigExtendedParams _$ConfigExtendedParamsFromJson(Map<String, dynamic> json) {
return _ConfigExtendedParams.fromJson(json);
}
/// @nodoc
mixin _$ConfigExtendedParams {
@JsonKey(name: "is-patch")
bool get isPatch => throw _privateConstructorUsedError;
@JsonKey(name: "is-compatible")
bool get isCompatible => throw _privateConstructorUsedError;
@JsonKey(name: "selected-map")
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
@JsonKey(name: "test-url")
String get testUrl => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ConfigExtendedParamsCopyWith<ConfigExtendedParams> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ConfigExtendedParamsCopyWith<$Res> {
factory $ConfigExtendedParamsCopyWith(ConfigExtendedParams value,
$Res Function(ConfigExtendedParams) then) =
_$ConfigExtendedParamsCopyWithImpl<$Res, ConfigExtendedParams>;
@useResult
$Res call(
{@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible,
@JsonKey(name: "selected-map") Map<String, String> selectedMap,
@JsonKey(name: "test-url") String testUrl});
}
/// @nodoc
class _$ConfigExtendedParamsCopyWithImpl<$Res,
$Val extends ConfigExtendedParams>
implements $ConfigExtendedParamsCopyWith<$Res> {
_$ConfigExtendedParamsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isPatch = null,
Object? isCompatible = null,
Object? selectedMap = null,
Object? testUrl = null,
}) {
return _then(_value.copyWith(
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
testUrl: null == testUrl
? _value.testUrl
: testUrl // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ConfigExtendedParamsImplCopyWith<$Res>
implements $ConfigExtendedParamsCopyWith<$Res> {
factory _$$ConfigExtendedParamsImplCopyWith(_$ConfigExtendedParamsImpl value,
$Res Function(_$ConfigExtendedParamsImpl) then) =
__$$ConfigExtendedParamsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible,
@JsonKey(name: "selected-map") Map<String, String> selectedMap,
@JsonKey(name: "test-url") String testUrl});
}
/// @nodoc
class __$$ConfigExtendedParamsImplCopyWithImpl<$Res>
extends _$ConfigExtendedParamsCopyWithImpl<$Res, _$ConfigExtendedParamsImpl>
implements _$$ConfigExtendedParamsImplCopyWith<$Res> {
__$$ConfigExtendedParamsImplCopyWithImpl(_$ConfigExtendedParamsImpl _value,
$Res Function(_$ConfigExtendedParamsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isPatch = null,
Object? isCompatible = null,
Object? selectedMap = null,
Object? testUrl = null,
}) {
return _then(_$ConfigExtendedParamsImpl(
isPatch: null == isPatch
? _value.isPatch
: isPatch // ignore: cast_nullable_to_non_nullable
as bool,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
testUrl: null == testUrl
? _value.testUrl
: testUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams {
const _$ConfigExtendedParamsImpl(
{@JsonKey(name: "is-patch") required this.isPatch,
@JsonKey(name: "is-compatible") required this.isCompatible,
@JsonKey(name: "selected-map")
required final Map<String, String> selectedMap,
@JsonKey(name: "test-url") required this.testUrl})
: _selectedMap = selectedMap;
factory _$ConfigExtendedParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$ConfigExtendedParamsImplFromJson(json);
@override
@JsonKey(name: "is-patch")
final bool isPatch;
@override
@JsonKey(name: "is-compatible")
final bool isCompatible;
final Map<String, String> _selectedMap;
@override
@JsonKey(name: "selected-map")
Map<String, String> get selectedMap {
if (_selectedMap is EqualUnmodifiableMapView) return _selectedMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_selectedMap);
}
@override
@JsonKey(name: "test-url")
final String testUrl;
@override
String toString() {
return 'ConfigExtendedParams(isPatch: $isPatch, isCompatible: $isCompatible, selectedMap: $selectedMap, testUrl: $testUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ConfigExtendedParamsImpl &&
(identical(other.isPatch, isPatch) || other.isPatch == isPatch) &&
(identical(other.isCompatible, isCompatible) ||
other.isCompatible == isCompatible) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap) &&
(identical(other.testUrl, testUrl) || other.testUrl == testUrl));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, isPatch, isCompatible,
const DeepCollectionEquality().hash(_selectedMap), testUrl);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ConfigExtendedParamsImplCopyWith<_$ConfigExtendedParamsImpl>
get copyWith =>
__$$ConfigExtendedParamsImplCopyWithImpl<_$ConfigExtendedParamsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ConfigExtendedParamsImplToJson(
this,
);
}
}
abstract class _ConfigExtendedParams implements ConfigExtendedParams {
const factory _ConfigExtendedParams(
{@JsonKey(name: "is-patch") required final bool isPatch,
@JsonKey(name: "is-compatible") required final bool isCompatible,
@JsonKey(name: "selected-map")
required final Map<String, String> selectedMap,
@JsonKey(name: "test-url") required final String testUrl}) =
_$ConfigExtendedParamsImpl;
factory _ConfigExtendedParams.fromJson(Map<String, dynamic> json) =
_$ConfigExtendedParamsImpl.fromJson;
@override
@JsonKey(name: "is-patch")
bool get isPatch;
@override
@JsonKey(name: "is-compatible")
bool get isCompatible;
@override
@JsonKey(name: "selected-map")
Map<String, String> get selectedMap;
@override
@JsonKey(name: "test-url")
String get testUrl;
@override
@JsonKey(ignore: true)
_$$ConfigExtendedParamsImplCopyWith<_$ConfigExtendedParamsImpl>
get copyWith => throw _privateConstructorUsedError;
}
UpdateConfigParams _$UpdateConfigParamsFromJson(Map<String, dynamic> json) { UpdateConfigParams _$UpdateConfigParamsFromJson(Map<String, dynamic> json) {
return _UpdateConfigParams.fromJson(json); return _UpdateConfigParams.fromJson(json);
} }
@@ -23,10 +251,7 @@ mixin _$UpdateConfigParams {
@JsonKey(name: "profile-path") @JsonKey(name: "profile-path")
String? get profilePath => throw _privateConstructorUsedError; String? get profilePath => throw _privateConstructorUsedError;
ClashConfig get config => throw _privateConstructorUsedError; ClashConfig get config => throw _privateConstructorUsedError;
@JsonKey(name: "is-patch") ConfigExtendedParams get params => throw _privateConstructorUsedError;
bool get isPatch => throw _privateConstructorUsedError;
@JsonKey(name: "is-compatible")
bool get isCompatible => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -43,8 +268,9 @@ abstract class $UpdateConfigParamsCopyWith<$Res> {
$Res call( $Res call(
{@JsonKey(name: "profile-path") String? profilePath, {@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config, ClashConfig config,
@JsonKey(name: "is-patch") bool isPatch, ConfigExtendedParams params});
@JsonKey(name: "is-compatible") bool isCompatible});
$ConfigExtendedParamsCopyWith<$Res> get params;
} }
/// @nodoc /// @nodoc
@@ -62,8 +288,7 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
$Res call({ $Res call({
Object? profilePath = freezed, Object? profilePath = freezed,
Object? config = null, Object? config = null,
Object? isPatch = null, Object? params = null,
Object? isCompatible = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
profilePath: freezed == profilePath profilePath: freezed == profilePath
@@ -74,16 +299,20 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
? _value.config ? _value.config
: config // ignore: cast_nullable_to_non_nullable : config // ignore: cast_nullable_to_non_nullable
as ClashConfig, as ClashConfig,
isPatch: null == isPatch params: null == params
? _value.isPatch ? _value.params
: isPatch // ignore: cast_nullable_to_non_nullable : params // ignore: cast_nullable_to_non_nullable
as bool, as ConfigExtendedParams,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val); ) as $Val);
} }
@override
@pragma('vm:prefer-inline')
$ConfigExtendedParamsCopyWith<$Res> get params {
return $ConfigExtendedParamsCopyWith<$Res>(_value.params, (value) {
return _then(_value.copyWith(params: value) as $Val);
});
}
} }
/// @nodoc /// @nodoc
@@ -97,8 +326,10 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res>
$Res call( $Res call(
{@JsonKey(name: "profile-path") String? profilePath, {@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config, ClashConfig config,
@JsonKey(name: "is-patch") bool isPatch, ConfigExtendedParams params});
@JsonKey(name: "is-compatible") bool isCompatible});
@override
$ConfigExtendedParamsCopyWith<$Res> get params;
} }
/// @nodoc /// @nodoc
@@ -114,8 +345,7 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? profilePath = freezed, Object? profilePath = freezed,
Object? config = null, Object? config = null,
Object? isPatch = null, Object? params = null,
Object? isCompatible = null,
}) { }) {
return _then(_$UpdateConfigParamsImpl( return _then(_$UpdateConfigParamsImpl(
profilePath: freezed == profilePath profilePath: freezed == profilePath
@@ -126,14 +356,10 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
? _value.config ? _value.config
: config // ignore: cast_nullable_to_non_nullable : config // ignore: cast_nullable_to_non_nullable
as ClashConfig, as ClashConfig,
isPatch: null == isPatch params: null == params
? _value.isPatch ? _value.params
: isPatch // ignore: cast_nullable_to_non_nullable : params // ignore: cast_nullable_to_non_nullable
as bool, as ConfigExtendedParams,
isCompatible: null == isCompatible
? _value.isCompatible
: isCompatible // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
} }
@@ -144,8 +370,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
const _$UpdateConfigParamsImpl( const _$UpdateConfigParamsImpl(
{@JsonKey(name: "profile-path") this.profilePath, {@JsonKey(name: "profile-path") this.profilePath,
required this.config, required this.config,
@JsonKey(name: "is-patch") required this.isPatch, required this.params});
@JsonKey(name: "is-compatible") required this.isCompatible});
factory _$UpdateConfigParamsImpl.fromJson(Map<String, dynamic> json) => factory _$UpdateConfigParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$UpdateConfigParamsImplFromJson(json); _$$UpdateConfigParamsImplFromJson(json);
@@ -156,15 +381,11 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
@override @override
final ClashConfig config; final ClashConfig config;
@override @override
@JsonKey(name: "is-patch") final ConfigExtendedParams params;
final bool isPatch;
@override
@JsonKey(name: "is-compatible")
final bool isCompatible;
@override @override
String toString() { String toString() {
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, isPatch: $isPatch, isCompatible: $isCompatible)'; return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)';
} }
@override @override
@@ -175,15 +396,12 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
(identical(other.profilePath, profilePath) || (identical(other.profilePath, profilePath) ||
other.profilePath == profilePath) && other.profilePath == profilePath) &&
(identical(other.config, config) || other.config == config) && (identical(other.config, config) || other.config == config) &&
(identical(other.isPatch, isPatch) || other.isPatch == isPatch) && (identical(other.params, params) || other.params == params));
(identical(other.isCompatible, isCompatible) ||
other.isCompatible == isCompatible));
} }
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => int get hashCode => Object.hash(runtimeType, profilePath, config, params);
Object.hash(runtimeType, profilePath, config, isPatch, isCompatible);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@@ -202,11 +420,9 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
abstract class _UpdateConfigParams implements UpdateConfigParams { abstract class _UpdateConfigParams implements UpdateConfigParams {
const factory _UpdateConfigParams( const factory _UpdateConfigParams(
{@JsonKey(name: "profile-path") final String? profilePath, {@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config, required final ClashConfig config,
@JsonKey(name: "is-patch") required final bool isPatch, required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl;
@JsonKey(name: "is-compatible") required final bool isCompatible}) =
_$UpdateConfigParamsImpl;
factory _UpdateConfigParams.fromJson(Map<String, dynamic> json) = factory _UpdateConfigParams.fromJson(Map<String, dynamic> json) =
_$UpdateConfigParamsImpl.fromJson; _$UpdateConfigParamsImpl.fromJson;
@@ -217,11 +433,7 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
@override @override
ClashConfig get config; ClashConfig get config;
@override @override
@JsonKey(name: "is-patch") ConfigExtendedParams get params;
bool get isPatch;
@override
@JsonKey(name: "is-compatible")
bool get isCompatible;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith => _$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith =>
@@ -398,32 +610,34 @@ abstract class _ChangeProxyParams implements ChangeProxyParams {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
Message _$MessageFromJson(Map<String, dynamic> json) { AppMessage _$AppMessageFromJson(Map<String, dynamic> json) {
return _Message.fromJson(json); return _AppMessage.fromJson(json);
} }
/// @nodoc /// @nodoc
mixin _$Message { mixin _$AppMessage {
MessageType get type => throw _privateConstructorUsedError; AppMessageType get type => throw _privateConstructorUsedError;
dynamic get data => throw _privateConstructorUsedError; dynamic get data => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$MessageCopyWith<Message> get copyWith => throw _privateConstructorUsedError; $AppMessageCopyWith<AppMessage> get copyWith =>
throw _privateConstructorUsedError;
} }
/// @nodoc /// @nodoc
abstract class $MessageCopyWith<$Res> { abstract class $AppMessageCopyWith<$Res> {
factory $MessageCopyWith(Message value, $Res Function(Message) then) = factory $AppMessageCopyWith(
_$MessageCopyWithImpl<$Res, Message>; AppMessage value, $Res Function(AppMessage) then) =
_$AppMessageCopyWithImpl<$Res, AppMessage>;
@useResult @useResult
$Res call({MessageType type, dynamic data}); $Res call({AppMessageType type, dynamic data});
} }
/// @nodoc /// @nodoc
class _$MessageCopyWithImpl<$Res, $Val extends Message> class _$AppMessageCopyWithImpl<$Res, $Val extends AppMessage>
implements $MessageCopyWith<$Res> { implements $AppMessageCopyWith<$Res> {
_$MessageCopyWithImpl(this._value, this._then); _$AppMessageCopyWithImpl(this._value, this._then);
// ignore: unused_field // ignore: unused_field
final $Val _value; final $Val _value;
@@ -440,7 +654,7 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message>
type: null == type type: null == type
? _value.type ? _value.type
: type // ignore: cast_nullable_to_non_nullable : type // ignore: cast_nullable_to_non_nullable
as MessageType, as AppMessageType,
data: freezed == data data: freezed == data
? _value.data ? _value.data
: data // ignore: cast_nullable_to_non_nullable : data // ignore: cast_nullable_to_non_nullable
@@ -450,21 +664,22 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message>
} }
/// @nodoc /// @nodoc
abstract class _$$MessageImplCopyWith<$Res> implements $MessageCopyWith<$Res> { abstract class _$$AppMessageImplCopyWith<$Res>
factory _$$MessageImplCopyWith( implements $AppMessageCopyWith<$Res> {
_$MessageImpl value, $Res Function(_$MessageImpl) then) = factory _$$AppMessageImplCopyWith(
__$$MessageImplCopyWithImpl<$Res>; _$AppMessageImpl value, $Res Function(_$AppMessageImpl) then) =
__$$AppMessageImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({MessageType type, dynamic data}); $Res call({AppMessageType type, dynamic data});
} }
/// @nodoc /// @nodoc
class __$$MessageImplCopyWithImpl<$Res> class __$$AppMessageImplCopyWithImpl<$Res>
extends _$MessageCopyWithImpl<$Res, _$MessageImpl> extends _$AppMessageCopyWithImpl<$Res, _$AppMessageImpl>
implements _$$MessageImplCopyWith<$Res> { implements _$$AppMessageImplCopyWith<$Res> {
__$$MessageImplCopyWithImpl( __$$AppMessageImplCopyWithImpl(
_$MessageImpl _value, $Res Function(_$MessageImpl) _then) _$AppMessageImpl _value, $Res Function(_$AppMessageImpl) _then)
: super(_value, _then); : super(_value, _then);
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@@ -473,11 +688,11 @@ class __$$MessageImplCopyWithImpl<$Res>
Object? type = null, Object? type = null,
Object? data = freezed, Object? data = freezed,
}) { }) {
return _then(_$MessageImpl( return _then(_$AppMessageImpl(
type: null == type type: null == type
? _value.type ? _value.type
: type // ignore: cast_nullable_to_non_nullable : type // ignore: cast_nullable_to_non_nullable
as MessageType, as AppMessageType,
data: freezed == data data: freezed == data
? _value.data ? _value.data
: data // ignore: cast_nullable_to_non_nullable : data // ignore: cast_nullable_to_non_nullable
@@ -488,27 +703,27 @@ class __$$MessageImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$MessageImpl implements _Message { class _$AppMessageImpl implements _AppMessage {
const _$MessageImpl({required this.type, this.data}); const _$AppMessageImpl({required this.type, this.data});
factory _$MessageImpl.fromJson(Map<String, dynamic> json) => factory _$AppMessageImpl.fromJson(Map<String, dynamic> json) =>
_$$MessageImplFromJson(json); _$$AppMessageImplFromJson(json);
@override @override
final MessageType type; final AppMessageType type;
@override @override
final dynamic data; final dynamic data;
@override @override
String toString() { String toString() {
return 'Message(type: $type, data: $data)'; return 'AppMessage(type: $type, data: $data)';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$MessageImpl && other is _$AppMessageImpl &&
(identical(other.type, type) || other.type == type) && (identical(other.type, type) || other.type == type) &&
const DeepCollectionEquality().equals(other.data, data)); const DeepCollectionEquality().equals(other.data, data));
} }
@@ -521,30 +736,188 @@ class _$MessageImpl implements _Message {
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$MessageImplCopyWith<_$MessageImpl> get copyWith => _$$AppMessageImplCopyWith<_$AppMessageImpl> get copyWith =>
__$$MessageImplCopyWithImpl<_$MessageImpl>(this, _$identity); __$$AppMessageImplCopyWithImpl<_$AppMessageImpl>(this, _$identity);
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$$MessageImplToJson( return _$$AppMessageImplToJson(
this, this,
); );
} }
} }
abstract class _Message implements Message { abstract class _AppMessage implements AppMessage {
const factory _Message( const factory _AppMessage(
{required final MessageType type, final dynamic data}) = _$MessageImpl; {required final AppMessageType type,
final dynamic data}) = _$AppMessageImpl;
factory _Message.fromJson(Map<String, dynamic> json) = _$MessageImpl.fromJson; factory _AppMessage.fromJson(Map<String, dynamic> json) =
_$AppMessageImpl.fromJson;
@override @override
MessageType get type; AppMessageType get type;
@override @override
dynamic get data; dynamic get data;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$MessageImplCopyWith<_$MessageImpl> get copyWith => _$$AppMessageImplCopyWith<_$AppMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ServiceMessage _$ServiceMessageFromJson(Map<String, dynamic> json) {
return _ServiceMessage.fromJson(json);
}
/// @nodoc
mixin _$ServiceMessage {
ServiceMessageType get type => throw _privateConstructorUsedError;
dynamic get data => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ServiceMessageCopyWith<ServiceMessage> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ServiceMessageCopyWith<$Res> {
factory $ServiceMessageCopyWith(
ServiceMessage value, $Res Function(ServiceMessage) then) =
_$ServiceMessageCopyWithImpl<$Res, ServiceMessage>;
@useResult
$Res call({ServiceMessageType type, dynamic data});
}
/// @nodoc
class _$ServiceMessageCopyWithImpl<$Res, $Val extends ServiceMessage>
implements $ServiceMessageCopyWith<$Res> {
_$ServiceMessageCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? type = null,
Object? data = freezed,
}) {
return _then(_value.copyWith(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as ServiceMessageType,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as dynamic,
) as $Val);
}
}
/// @nodoc
abstract class _$$ServiceMessageImplCopyWith<$Res>
implements $ServiceMessageCopyWith<$Res> {
factory _$$ServiceMessageImplCopyWith(_$ServiceMessageImpl value,
$Res Function(_$ServiceMessageImpl) then) =
__$$ServiceMessageImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({ServiceMessageType type, dynamic data});
}
/// @nodoc
class __$$ServiceMessageImplCopyWithImpl<$Res>
extends _$ServiceMessageCopyWithImpl<$Res, _$ServiceMessageImpl>
implements _$$ServiceMessageImplCopyWith<$Res> {
__$$ServiceMessageImplCopyWithImpl(
_$ServiceMessageImpl _value, $Res Function(_$ServiceMessageImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? type = null,
Object? data = freezed,
}) {
return _then(_$ServiceMessageImpl(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as ServiceMessageType,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as dynamic,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ServiceMessageImpl implements _ServiceMessage {
const _$ServiceMessageImpl({required this.type, this.data});
factory _$ServiceMessageImpl.fromJson(Map<String, dynamic> json) =>
_$$ServiceMessageImplFromJson(json);
@override
final ServiceMessageType type;
@override
final dynamic data;
@override
String toString() {
return 'ServiceMessage(type: $type, data: $data)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ServiceMessageImpl &&
(identical(other.type, type) || other.type == type) &&
const DeepCollectionEquality().equals(other.data, data));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, type, const DeepCollectionEquality().hash(data));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith =>
__$$ServiceMessageImplCopyWithImpl<_$ServiceMessageImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ServiceMessageImplToJson(
this,
);
}
}
abstract class _ServiceMessage implements ServiceMessage {
const factory _ServiceMessage(
{required final ServiceMessageType type,
final dynamic data}) = _$ServiceMessageImpl;
factory _ServiceMessage.fromJson(Map<String, dynamic> json) =
_$ServiceMessageImpl.fromJson;
@override
ServiceMessageType get type;
@override
dynamic get data;
@override
@JsonKey(ignore: true)
_$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@@ -1006,6 +1379,151 @@ abstract class _Process implements Process {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
Fd _$FdFromJson(Map<String, dynamic> json) {
return _Fd.fromJson(json);
}
/// @nodoc
mixin _$Fd {
int get id => throw _privateConstructorUsedError;
int get value => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$FdCopyWith<Fd> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FdCopyWith<$Res> {
factory $FdCopyWith(Fd value, $Res Function(Fd) then) =
_$FdCopyWithImpl<$Res, Fd>;
@useResult
$Res call({int id, int value});
}
/// @nodoc
class _$FdCopyWithImpl<$Res, $Val extends Fd> implements $FdCopyWith<$Res> {
_$FdCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? value = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$FdImplCopyWith<$Res> implements $FdCopyWith<$Res> {
factory _$$FdImplCopyWith(_$FdImpl value, $Res Function(_$FdImpl) then) =
__$$FdImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int id, int value});
}
/// @nodoc
class __$$FdImplCopyWithImpl<$Res> extends _$FdCopyWithImpl<$Res, _$FdImpl>
implements _$$FdImplCopyWith<$Res> {
__$$FdImplCopyWithImpl(_$FdImpl _value, $Res Function(_$FdImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? value = null,
}) {
return _then(_$FdImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$FdImpl implements _Fd {
const _$FdImpl({required this.id, required this.value});
factory _$FdImpl.fromJson(Map<String, dynamic> json) =>
_$$FdImplFromJson(json);
@override
final int id;
@override
final int value;
@override
String toString() {
return 'Fd(id: $id, value: $value)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FdImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.value, value) || other.value == value));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, id, value);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$FdImplCopyWith<_$FdImpl> get copyWith =>
__$$FdImplCopyWithImpl<_$FdImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$FdImplToJson(
this,
);
}
}
abstract class _Fd implements Fd {
const factory _Fd({required final int id, required final int value}) =
_$FdImpl;
factory _Fd.fromJson(Map<String, dynamic> json) = _$FdImpl.fromJson;
@override
int get id;
@override
int get value;
@override
@JsonKey(ignore: true)
_$$FdImplCopyWith<_$FdImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ProcessMapItem _$ProcessMapItemFromJson(Map<String, dynamic> json) { ProcessMapItem _$ProcessMapItemFromJson(Map<String, dynamic> json) {
return _ProcessMapItem.fromJson(json); return _ProcessMapItem.fromJson(json);
} }

View File

@@ -6,13 +6,31 @@ part of '../ffi.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_$ConfigExtendedParamsImpl _$$ConfigExtendedParamsImplFromJson(
Map<String, dynamic> json) =>
_$ConfigExtendedParamsImpl(
isPatch: json['is-patch'] as bool,
isCompatible: json['is-compatible'] as bool,
selectedMap: Map<String, String>.from(json['selected-map'] as Map),
testUrl: json['test-url'] as String,
);
Map<String, dynamic> _$$ConfigExtendedParamsImplToJson(
_$ConfigExtendedParamsImpl instance) =>
<String, dynamic>{
'is-patch': instance.isPatch,
'is-compatible': instance.isCompatible,
'selected-map': instance.selectedMap,
'test-url': instance.testUrl,
};
_$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson( _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$UpdateConfigParamsImpl( _$UpdateConfigParamsImpl(
profilePath: json['profile-path'] as String?, profilePath: json['profile-path'] as String?,
config: ClashConfig.fromJson(json['config'] as Map<String, dynamic>), config: ClashConfig.fromJson(json['config'] as Map<String, dynamic>),
isPatch: json['is-patch'] as bool, params:
isCompatible: json['is-compatible'] as bool, ConfigExtendedParams.fromJson(json['params'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$$UpdateConfigParamsImplToJson( Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
@@ -20,8 +38,7 @@ Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
<String, dynamic>{ <String, dynamic>{
'profile-path': instance.profilePath, 'profile-path': instance.profilePath,
'config': instance.config, 'config': instance.config,
'is-patch': instance.isPatch, 'params': instance.params,
'is-compatible': instance.isCompatible,
}; };
_$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson( _$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson(
@@ -38,26 +55,44 @@ Map<String, dynamic> _$$ChangeProxyParamsImplToJson(
'proxy-name': instance.proxyName, 'proxy-name': instance.proxyName,
}; };
_$MessageImpl _$$MessageImplFromJson(Map<String, dynamic> json) => _$AppMessageImpl _$$AppMessageImplFromJson(Map<String, dynamic> json) =>
_$MessageImpl( _$AppMessageImpl(
type: $enumDecode(_$MessageTypeEnumMap, json['type']), type: $enumDecode(_$AppMessageTypeEnumMap, json['type']),
data: json['data'], data: json['data'],
); );
Map<String, dynamic> _$$MessageImplToJson(_$MessageImpl instance) => Map<String, dynamic> _$$AppMessageImplToJson(_$AppMessageImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'type': _$MessageTypeEnumMap[instance.type]!, 'type': _$AppMessageTypeEnumMap[instance.type]!,
'data': instance.data, 'data': instance.data,
}; };
const _$MessageTypeEnumMap = { const _$AppMessageTypeEnumMap = {
MessageType.log: 'log', AppMessageType.log: 'log',
MessageType.tun: 'tun', AppMessageType.delay: 'delay',
MessageType.delay: 'delay', AppMessageType.request: 'request',
MessageType.process: 'process', AppMessageType.started: 'started',
MessageType.now: 'now', AppMessageType.loaded: 'loaded',
MessageType.request: 'request', };
MessageType.run: 'run',
_$ServiceMessageImpl _$$ServiceMessageImplFromJson(Map<String, dynamic> json) =>
_$ServiceMessageImpl(
type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']),
data: json['data'],
);
Map<String, dynamic> _$$ServiceMessageImplToJson(
_$ServiceMessageImpl instance) =>
<String, dynamic>{
'type': _$ServiceMessageTypeEnumMap[instance.type]!,
'data': instance.data,
};
const _$ServiceMessageTypeEnumMap = {
ServiceMessageType.protect: 'protect',
ServiceMessageType.process: 'process',
ServiceMessageType.started: 'started',
ServiceMessageType.loaded: 'loaded',
}; };
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl( _$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
@@ -93,6 +128,16 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
'metadata': instance.metadata, 'metadata': instance.metadata,
}; };
_$FdImpl _$$FdImplFromJson(Map<String, dynamic> json) => _$FdImpl(
id: (json['id'] as num).toInt(),
value: (json['value'] as num).toInt(),
);
Map<String, dynamic> _$$FdImplToJson(_$FdImpl instance) => <String, dynamic>{
'id': instance.id,
'value': instance.value,
};
_$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) => _$ProcessMapItemImpl _$$ProcessMapItemImplFromJson(Map<String, dynamic> json) =>
_$ProcessMapItemImpl( _$ProcessMapItemImpl(
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),

View File

@@ -161,6 +161,7 @@ mixin _$CheckIpSelectorState {
bool get isInit => throw _privateConstructorUsedError; bool get isInit => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError; bool get isStart => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError; Map<String, String> get selectedMap => throw _privateConstructorUsedError;
num get checkIpNum => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$CheckIpSelectorStateCopyWith<CheckIpSelectorState> get copyWith => $CheckIpSelectorStateCopyWith<CheckIpSelectorState> get copyWith =>
@@ -173,7 +174,11 @@ abstract class $CheckIpSelectorStateCopyWith<$Res> {
$Res Function(CheckIpSelectorState) then) = $Res Function(CheckIpSelectorState) then) =
_$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>; _$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>;
@useResult @useResult
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap}); $Res call(
{bool isInit,
bool isStart,
Map<String, String> selectedMap,
num checkIpNum});
} }
/// @nodoc /// @nodoc
@@ -193,6 +198,7 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res,
Object? isInit = null, Object? isInit = null,
Object? isStart = null, Object? isStart = null,
Object? selectedMap = null, Object? selectedMap = null,
Object? checkIpNum = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
isInit: null == isInit isInit: null == isInit
@@ -207,6 +213,10 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res,
? _value.selectedMap ? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable : selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>, as Map<String, String>,
checkIpNum: null == checkIpNum
? _value.checkIpNum
: checkIpNum // ignore: cast_nullable_to_non_nullable
as num,
) as $Val); ) as $Val);
} }
} }
@@ -219,7 +229,11 @@ abstract class _$$CheckIpSelectorStateImplCopyWith<$Res>
__$$CheckIpSelectorStateImplCopyWithImpl<$Res>; __$$CheckIpSelectorStateImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap}); $Res call(
{bool isInit,
bool isStart,
Map<String, String> selectedMap,
num checkIpNum});
} }
/// @nodoc /// @nodoc
@@ -236,6 +250,7 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
Object? isInit = null, Object? isInit = null,
Object? isStart = null, Object? isStart = null,
Object? selectedMap = null, Object? selectedMap = null,
Object? checkIpNum = null,
}) { }) {
return _then(_$CheckIpSelectorStateImpl( return _then(_$CheckIpSelectorStateImpl(
isInit: null == isInit isInit: null == isInit
@@ -250,6 +265,10 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
? _value._selectedMap ? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable : selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>, as Map<String, String>,
checkIpNum: null == checkIpNum
? _value.checkIpNum
: checkIpNum // ignore: cast_nullable_to_non_nullable
as num,
)); ));
} }
} }
@@ -260,7 +279,8 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
const _$CheckIpSelectorStateImpl( const _$CheckIpSelectorStateImpl(
{required this.isInit, {required this.isInit,
required this.isStart, required this.isStart,
required final Map<String, String> selectedMap}) required final Map<String, String> selectedMap,
required this.checkIpNum})
: _selectedMap = selectedMap; : _selectedMap = selectedMap;
@override @override
@@ -275,9 +295,12 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
return EqualUnmodifiableMapView(_selectedMap); return EqualUnmodifiableMapView(_selectedMap);
} }
@override
final num checkIpNum;
@override @override
String toString() { String toString() {
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap)'; return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap, checkIpNum: $checkIpNum)';
} }
@override @override
@@ -288,12 +311,14 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
(identical(other.isInit, isInit) || other.isInit == isInit) && (identical(other.isInit, isInit) || other.isInit == isInit) &&
(identical(other.isStart, isStart) || other.isStart == isStart) && (identical(other.isStart, isStart) || other.isStart == isStart) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap)); .equals(other._selectedMap, _selectedMap) &&
(identical(other.checkIpNum, checkIpNum) ||
other.checkIpNum == checkIpNum));
} }
@override @override
int get hashCode => Object.hash(runtimeType, isInit, isStart, int get hashCode => Object.hash(runtimeType, isInit, isStart,
const DeepCollectionEquality().hash(_selectedMap)); const DeepCollectionEquality().hash(_selectedMap), checkIpNum);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@@ -306,10 +331,10 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
abstract class _CheckIpSelectorState implements CheckIpSelectorState { abstract class _CheckIpSelectorState implements CheckIpSelectorState {
const factory _CheckIpSelectorState( const factory _CheckIpSelectorState(
{required final bool isInit, {required final bool isInit,
required final bool isStart, required final bool isStart,
required final Map<String, String> selectedMap}) = required final Map<String, String> selectedMap,
_$CheckIpSelectorStateImpl; required final num checkIpNum}) = _$CheckIpSelectorStateImpl;
@override @override
bool get isInit; bool get isInit;
@@ -318,6 +343,8 @@ abstract class _CheckIpSelectorState implements CheckIpSelectorState {
@override @override
Map<String, String> get selectedMap; Map<String, String> get selectedMap;
@override @override
num get checkIpNum;
@override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl> _$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;

View File

@@ -19,6 +19,7 @@ class CheckIpSelectorState with _$CheckIpSelectorState {
required bool isInit, required bool isInit,
required bool isStart, required bool isStart,
required SelectedMap selectedMap, required SelectedMap selectedMap,
required num checkIpNum
}) = _CheckIpSelectorState; }) = _CheckIpSelectorState;
} }

View File

@@ -1,24 +1,30 @@
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/constant.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@immutable
class SystemColorSchemes { class SystemColorSchemes {
SystemColorSchemes({ final ColorScheme? lightColorScheme;
ColorScheme? lightColorScheme, final ColorScheme? darkColorScheme;
ColorScheme? darkColorScheme,
}) : lightColorScheme = lightColorScheme ?? const SystemColorSchemes({
ColorScheme.fromSeed(seedColor: defaultPrimaryColor), this.lightColorScheme,
darkColorScheme = darkColorScheme ?? this.darkColorScheme,
ColorScheme.fromSeed( });
seedColor: defaultPrimaryColor,
brightness: Brightness.dark,
);
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
getSystemColorSchemeForBrightness(Brightness? brightness) { getSystemColorSchemeForBrightness(Brightness? brightness) {
if (brightness != null && brightness == Brightness.dark) { if (brightness != null && brightness == Brightness.dark) {
return darkColorScheme; return darkColorScheme != null
? ColorScheme.fromSeed(
seedColor: darkColorScheme!.primary,
brightness: brightness,
)
: ColorScheme.fromSeed(
seedColor: defaultPrimaryColor,
brightness: brightness,
);
} }
return lightColorScheme; return lightColorScheme != null
? ColorScheme.fromSeed(seedColor: darkColorScheme!.primary)
: ColorScheme.fromSeed(seedColor: defaultPrimaryColor);
} }
} }

View File

@@ -5,32 +5,30 @@ import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
class App { class App {
static App? _instance; static App? _instance;
MethodChannel? methodChannel; late MethodChannel methodChannel;
Function()? onExit; Function()? onExit;
App._internal() { App._internal() {
if (Platform.isAndroid) { methodChannel = const MethodChannel("app");
methodChannel = const MethodChannel("app"); methodChannel.setMethodCallHandler((call) async {
methodChannel!.setMethodCallHandler((call) async { switch (call.method) {
switch (call.method) { case "exit":
case "exit": if (onExit != null) {
if (onExit != null) { await onExit!();
await onExit!(); }
} case "gc":
break; clashCore.requestGc();
case "gc": default:
clashCore.requestGc(); throw MissingPluginException();
break; }
default: });
throw MissingPluginException();
}
});
}
} }
factory App() { factory App() {
@@ -39,12 +37,12 @@ class App {
} }
Future<bool?> moveTaskToBack() async { Future<bool?> moveTaskToBack() async {
return await methodChannel?.invokeMethod<bool>("moveTaskToBack"); return await methodChannel.invokeMethod<bool>("moveTaskToBack");
} }
Future<List<Package>> getPackages() async { Future<List<Package>> getPackages() async {
final packagesString = final packagesString =
await methodChannel?.invokeMethod<String>("getPackages"); await methodChannel.invokeMethod<String>("getPackages");
return Isolate.run<List<Package>>(() { return Isolate.run<List<Package>>(() {
final List<dynamic> packagesRaw = final List<dynamic> packagesRaw =
packagesString != null ? json.decode(packagesString) : []; packagesString != null ? json.decode(packagesString) : [];
@@ -53,7 +51,7 @@ class App {
} }
Future<ImageProvider?> getPackageIcon(String packageName) async { Future<ImageProvider?> getPackageIcon(String packageName) async {
final base64 = await methodChannel?.invokeMethod<String>("getPackageIcon", { final base64 = await methodChannel.invokeMethod<String>("getPackageIcon", {
"packageName": packageName, "packageName": packageName,
}); });
if (base64 == null) { if (base64 == null) {
@@ -63,13 +61,19 @@ class App {
} }
Future<bool?> tip(String? message) async { Future<bool?> tip(String? message) async {
return await methodChannel?.invokeMethod<bool>("tip", { return await methodChannel.invokeMethod<bool>("tip", {
"message": "$message", "message": "$message",
}); });
} }
Future<bool?> updateExcludeFromRecents(bool value) async {
return await methodChannel.invokeMethod<bool>("updateExcludeFromRecents", {
"value": value,
});
}
Future<String?> resolverProcess(Process process) async { Future<String?> resolverProcess(Process process) async {
return await methodChannel?.invokeMethod<String>("resolverProcess", { return await methodChannel.invokeMethod<String>("resolverProcess", {
"data": json.encode(process), "data": json.encode(process),
}); });
} }

View File

@@ -1,29 +1,29 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:fl_clash/clash/core.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/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:proxy/proxy_platform_interface.dart'; import 'package:proxy/proxy_platform_interface.dart';
class Proxy extends ProxyPlatform { class Proxy extends ProxyPlatform {
static Proxy? _instance; static Proxy? _instance;
late MethodChannel methodChannel; late MethodChannel methodChannel;
late ReceivePort receiver; ReceivePort? receiver;
ServiceMessageListener? _serviceMessageHandler;
Proxy._internal() { Proxy._internal() {
methodChannel = const MethodChannel("proxy"); methodChannel = const MethodChannel("proxy");
receiver = ReceivePort()
..listen(
(message) {
setProtect(int.parse(message));
},
);
methodChannel.setMethodCallHandler((call) async { methodChannel.setMethodCallHandler((call) async {
switch (call.method) { switch (call.method) {
case "startAfter": case "started":
int fd = call.arguments; final fd = call.arguments;
startAfterHook(fd); onStarted(fd);
break; break;
default: default:
throw MissingPluginException(); throw MissingPluginException();
@@ -36,16 +36,27 @@ class Proxy extends ProxyPlatform {
return _instance!; return _instance!;
} }
Future<bool?> _initService() async {
return await methodChannel.invokeMethod<bool>("initService");
}
handleStop() {
globalState.stopSystemProxy();
}
@override @override
Future<bool?> startProxy(int port, String? args) async { Future<bool?> startProxy(port, args) async {
if (!globalState.isVpnService) {
return await _initService();
}
return await methodChannel return await methodChannel
.invokeMethod<bool>("StartProxy", {'port': port, 'args': args}); .invokeMethod<bool>("startProxy", {'port': port, 'args': args});
} }
@override @override
Future<bool?> stopProxy() async { Future<bool?> stopProxy() async {
clashCore.stopTun(); clashCore.stopTun();
final isStop = await methodChannel.invokeMethod<bool>("StopProxy"); final isStop = await methodChannel.invokeMethod<bool>("stopProxy");
if (isStop == true) { if (isStop == true) {
startTime = null; startTime = null;
} }
@@ -53,11 +64,7 @@ class Proxy extends ProxyPlatform {
} }
Future<bool?> setProtect(int fd) async { Future<bool?> setProtect(int fd) async {
return await methodChannel.invokeMethod<bool?>("SetProtect", {'fd': fd}); return await methodChannel.invokeMethod<bool?>("setProtect", {'fd': fd});
}
Future<int?> getRunTimeStamp() async {
return await methodChannel.invokeMethod<int?>("GetRunTimeStamp");
} }
Future<bool?> startForeground({ Future<bool?> startForeground({
@@ -72,26 +79,39 @@ class Proxy extends ProxyPlatform {
bool get isStart => startTime != null && startTime!.isBeforeNow; bool get isStart => startTime != null && startTime!.isBeforeNow;
startAfterHook(int? fd) { onStarted(int? fd) {
if (!isStart && fd != null) { if (fd == null) return;
clashCore.startTun(fd); if (receiver != null) {
updateStartTime(); receiver!.close();
receiver == null;
} }
receiver = ReceivePort();
receiver!.listen((message) {
_handleServiceMessage(message);
});
clashCore.startTun(fd, receiver!.sendPort.nativePort);
} }
// updateStartTime() async { updateStartTime() {
// startTime = clashCore.getRunTime(); startTime = clashCore.getRunTime();
// }
updateStartTime() async {
startTime = await getRunTime();
} }
Future<DateTime?> getRunTime() async { setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
final runTimeStamp = await getRunTimeStamp(); _serviceMessageHandler = serviceMessageListener;
return runTimeStamp != null }
? DateTime.fromMillisecondsSinceEpoch(runTimeStamp)
: null; _handleServiceMessage(String message) {
final m = ServiceMessage.fromJson(json.decode(message));
switch (m.type) {
case ServiceMessageType.protect:
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
case ServiceMessageType.process:
_serviceMessageHandler?.onProcess(Process.fromJson(m.data));
case ServiceMessageType.started:
_serviceMessageHandler?.onStarted(m.data);
case ServiceMessageType.loaded:
_serviceMessageHandler?.onLoaded(m.data);
}
} }
} }

View File

@@ -5,16 +5,21 @@ import 'dart:io';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:fl_clash/clash/clash.dart'; 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/plugins/proxy.dart';
import 'package:fl_clash/widgets/scaffold.dart'; import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'controller.dart'; import 'controller.dart';
import 'models/models.dart'; import 'models/models.dart';
import 'common/common.dart'; import 'common/common.dart';
class GlobalState { class GlobalState {
Timer? timer; Timer? timer;
Timer? groupsUpdateTimer; Timer? groupsUpdateTimer;
var isVpnService = false;
late PackageInfo packageInfo;
Function? updateCurrentDelayDebounce; Function? updateCurrentDelayDebounce;
PageController? pageController; PageController? pageController;
final navigatorKey = GlobalKey<NavigatorState>(); final navigatorKey = GlobalKey<NavigatorState>();
@@ -43,13 +48,18 @@ class GlobalState {
}) async { }) async {
final profilePath = await appPath.getProfilePath(config.currentProfileId); final profilePath = await appPath.getProfilePath(config.currentProfileId);
await config.currentProfile?.checkAndUpdate(); await config.currentProfile?.checkAndUpdate();
debugPrint("update config"); final res = await clashCore.updateConfig(
final res = await clashCore.updateConfig(UpdateConfigParams( UpdateConfigParams(
profilePath: profilePath, profilePath: profilePath,
config: clashConfig, config: clashConfig,
isPatch: isPatch, params: ConfigExtendedParams(
isCompatible: config.isCompatible, isPatch: isPatch,
)); isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
testUrl: config.testUrl,
),
),
);
if (res.isNotEmpty) throw res; if (res.isNotEmpty) throw res;
} }
@@ -75,11 +85,16 @@ class GlobalState {
args: args, args: args,
); );
startListenUpdate(); startListenUpdate();
if (Platform.isAndroid) {
return;
}
applyProfile( applyProfile(
appState: appState, appState: appState,
config: config, config: config,
clashConfig: clashConfig, clashConfig: clashConfig,
); ).then((_){
globalState.appController.addCheckIpNumDebounce();
});
} }
Future<void> stopSystemProxy() async { Future<void> stopSystemProxy() async {
@@ -87,7 +102,7 @@ class GlobalState {
stopListenUpdate(); stopListenUpdate();
} }
Future<void> applyProfile({ Future applyProfile({
required AppState appState, required AppState appState,
required Config config, required Config config,
required ClashConfig clashConfig, required ClashConfig clashConfig,
@@ -97,12 +112,8 @@ class GlobalState {
config: config, config: config,
isPatch: false, isPatch: false,
); );
clashCore.setProfileName(config.currentProfile?.label ?? '');
await updateGroups(appState); await updateGroups(appState);
changeProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} }
init({ init({
@@ -120,25 +131,6 @@ class GlobalState {
updateCoreVersionInfo(appState); updateCoreVersionInfo(appState);
} }
changeProxy({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) {
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
}
Future<void> updateGroups(AppState appState) async { Future<void> updateGroups(AppState appState) async {
appState.groups = await clashCore.getProxiesGroups(); appState.groups = await clashCore.getProxiesGroups();
} }
@@ -197,20 +189,18 @@ class GlobalState {
updateTraffic({ updateTraffic({
AppState? appState, AppState? appState,
required Config config,
}) { }) {
final traffic = clashCore.getTraffic(); final traffic = clashCore.getTraffic();
if (appState != null) { if (Platform.isAndroid && isVpnService == true) {
appState.addTraffic(traffic); proxy?.startForeground(
appState.totalTraffic = clashCore.getTotalTraffic(); title: clashCore.getProfileName(),
}
if (Platform.isAndroid) {
final currentProfile = config.currentProfile;
if (currentProfile == null) return;
proxyManager.startForeground(
title: currentProfile.label ?? currentProfile.id,
content: "$traffic", content: "$traffic",
); );
} else {
if (appState != null) {
appState.addTraffic(traffic);
appState.totalTraffic = clashCore.getTotalTraffic();
}
} }
} }
@@ -219,30 +209,30 @@ class GlobalState {
required String message, required String message,
SnackBarAction? action, SnackBarAction? action,
}) { }) {
// final width = context.width; final width = context.width;
// EdgeInsets margin; EdgeInsets margin;
// if (width < 600) { if (width < 600) {
// margin = const EdgeInsets.only( margin = const EdgeInsets.only(
// bottom: 96, bottom: 16,
// right: 16, right: 16,
// left: 16, left: 16,
// ); );
// } else { } else {
// margin = EdgeInsets.only( margin = EdgeInsets.only(
// bottom: 16, bottom: 16,
// left: 16, left: 16,
// right: width - 316, right: width - 316,
// ); );
// } }
// ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
// SnackBar( SnackBar(
// action: action, action: action,
// content: Text(message), content: Text(message),
// behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
// duration: const Duration(milliseconds: 1500), duration: const Duration(milliseconds: 1500),
// margin: margin, margin: margin,
// ), ),
// ); );
} }
Future<T?> safeRun<T>( Future<T?> safeRun<T>(
@@ -263,7 +253,7 @@ class GlobalState {
} }
} }
int getColumns(ViewMode viewMode,int currentColumns){ int getColumns(ViewMode viewMode, int currentColumns) {
final targetColumnsArray = switch (viewMode) { final targetColumnsArray = switch (viewMode) {
ViewMode.mobile => [2, 1], ViewMode.mobile => [2, 1],
ViewMode.laptop => [3, 2], ViewMode.laptop => [3, 2],

View File

@@ -1,6 +1,9 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.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/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
class AndroidContainer extends StatefulWidget { class AndroidContainer extends StatefulWidget {
final Widget child; final Widget child;
@@ -17,6 +20,17 @@ class AndroidContainer extends StatefulWidget {
class _AndroidContainerState extends State<AndroidContainer> class _AndroidContainerState extends State<AndroidContainer>
with WidgetsBindingObserver { with WidgetsBindingObserver {
_excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.isExclude,
builder: (_, isExclude, child) {
app?.updateExcludeFromRecents(isExclude);
return child!;
},
child: child,
);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -34,7 +48,7 @@ class _AndroidContainerState extends State<AndroidContainer>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; return _excludeContainer(widget.child);
} }
@override @override
@@ -42,5 +56,4 @@ class _AndroidContainerState extends State<AndroidContainer>
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
} }

View File

@@ -12,17 +12,6 @@ class AppStateContainer extends StatelessWidget {
required this.child, required this.child,
}); });
_autoLaunchContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.autoLaunch,
builder: (_, isAutoLaunch, child) {
autoLaunch?.updateStatus(isAutoLaunch);
return child!;
},
child: child,
);
}
_updateNavigationsContainer(Widget child) { _updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>( return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) { selector: (_, appState, config) {
@@ -51,10 +40,8 @@ class AppStateContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _autoLaunchContainer( return _updateNavigationsContainer(
_updateNavigationsContainer( child,
child,
),
); );
} }
} }

View File

@@ -6,43 +6,70 @@ import 'text.dart';
class Info { class Info {
final String label; final String label;
final IconData iconData; final IconData? iconData;
const Info({ const Info({
required this.label, required this.label,
required this.iconData, this.iconData,
}); });
} }
class InfoHeader extends StatelessWidget { class InfoHeader extends StatelessWidget {
final Info info; final Info info;
final List<Widget> actions;
const InfoHeader({ const InfoHeader({
super.key, super.key,
required this.info, required this.info,
}); List<Widget>? actions,
}) : actions = actions ?? const [];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Icon( Row(
info.iconData, mainAxisSize: MainAxisSize.min,
color: Theme.of(context).colorScheme.primary, children: [
), if (info.iconData != null) ...[
const SizedBox( Icon(
width: 8, info.iconData,
), color: Theme
Flexible( .of(context)
child: TooltipText( .colorScheme
text: Text( .primary,
info.label, ),
maxLines: 1, const SizedBox(
overflow: TextOverflow.ellipsis, width: 8,
style: Theme.of(context).textTheme.titleMedium, ),
],
Flexible(
child: TooltipText(
text: Text(
info.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme
.of(context)
.textTheme
.titleMedium,
),
),
), ),
],
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...actions,
],
), ),
), ),
], ],
@@ -70,10 +97,12 @@ class CommonCard extends StatelessWidget {
final CommonCardType type; final CommonCardType type;
BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) { BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) {
if(type == CommonCardType.filled){ if (type == CommonCardType.filled) {
return BorderSide.none; return BorderSide.none;
} }
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme
.of(context)
.colorScheme;
final hoverColor = isSelected final hoverColor = isSelected
? colorScheme.primary.toLight() ? colorScheme.primary.toLight()
: colorScheme.primary.toLighter(); : colorScheme.primary.toLighter();
@@ -85,14 +114,15 @@ class CommonCard extends StatelessWidget {
); );
} }
return BorderSide( return BorderSide(
color: color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
); );
} }
Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) { Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme
switch(type){ .of(context)
.colorScheme;
switch (type) {
case CommonCardType.plain: case CommonCardType.plain:
if (isSelected) { if (isSelected) {
return colorScheme.secondaryContainer; return colorScheme.secondaryContainer;
@@ -100,7 +130,8 @@ class CommonCard extends StatelessWidget {
if (states.isEmpty) { if (states.isEmpty) {
return colorScheme.secondaryContainer.toLittle(); return colorScheme.secondaryContainer.toLittle();
} }
return Theme.of(context) return Theme
.of(context)
.outlinedButtonTheme .outlinedButtonTheme
.style .style
?.backgroundColor ?.backgroundColor
@@ -147,10 +178,10 @@ class CommonCard extends StatelessWidget {
), ),
), ),
backgroundColor: WidgetStateProperty.resolveWith( backgroundColor: WidgetStateProperty.resolveWith(
(states) => getBackgroundColor(context, states), (states) => getBackgroundColor(context, states),
), ),
side: WidgetStateProperty.resolveWith( side: WidgetStateProperty.resolveWith(
(states) => getBorderSide(context, states), (states) => getBorderSide(context, states),
), ),
), ),
onPressed: onPressed, onPressed: onPressed,
@@ -180,7 +211,10 @@ class SelectIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: Theme.of(context).colorScheme.inversePrimary, color: Theme
.of(context)
.colorScheme
.inversePrimary,
shape: const CircleBorder(), shape: const CircleBorder(),
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),

View File

@@ -20,7 +20,7 @@ class CommonChip extends StatelessWidget {
if (type == ChipType.delete) { if (type == ChipType.delete) {
return Chip( return Chip(
avatar: avatar, avatar: avatar,
padding: const EdgeInsets.symmetric( labelPadding:const EdgeInsets.symmetric(
vertical: 0, vertical: 0,
horizontal: 4, horizontal: 4,
), ),
@@ -35,7 +35,7 @@ class CommonChip extends StatelessWidget {
return ActionChip( return ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
avatar: avatar, avatar: avatar,
padding: const EdgeInsets.symmetric( labelPadding:const EdgeInsets.symmetric(
vertical: 0, vertical: 0,
horizontal: 4, horizontal: 4,
), ),

View File

@@ -1,5 +1,4 @@
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/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -19,7 +18,7 @@ class ClashMessageContainer extends StatefulWidget {
} }
class _ClashMessageContainerState extends State<ClashMessageContainer> class _ClashMessageContainerState extends State<ClashMessageContainer>
with ClashMessageListener { with AppMessageListener {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; return widget.child;
@@ -50,24 +49,6 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
super.onLog(log); super.onLog(log);
} }
@override
void onTun(String fd) {
proxyManager.setProtect(int.parse(fd));
super.onTun(fd);
}
@override
void onProcess(Process process) async {
var packageName = await app?.resolverProcess(process);
clashCore.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
super.onProcess(process);
}
@override @override
void onRequest(Connection connection) async { void onRequest(Connection connection) async {
globalState.appController.appState.addRequest(connection); globalState.appController.appState.addRequest(connection);
@@ -75,8 +56,28 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
} }
@override @override
void onRun(String runTime) async { void onLoaded(String groupName) {
// proxy?.updateStartTime(); final appController = globalState.appController;
super.onRun(runTime); final currentSelectedMap = appController.config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
);
appController.addCheckIpNumDebounce();
super.onLoaded(proxyName);
}
@override
void onStarted(String runTime) {
super.onStarted(runTime);
proxy?.updateStartTime();
final appController = globalState.appController;
appController.rawApplyProfile().then((_) {
appController.addCheckIpNumDebounce();
});
} }
} }

View File

@@ -1,8 +1,10 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/open_container.dart'; import 'package:fl_clash/widgets/open_container.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'card.dart';
import 'extend_page.dart'; import 'extend_page.dart';
import 'scaffold.dart'; import 'scaffold.dart';
@@ -214,7 +216,8 @@ class ListItem<T> extends StatelessWidget {
return OpenContainer( return OpenContainer(
closedBuilder: (_, action) { closedBuilder: (_, action) {
openAction() { openAction() {
final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) { if (!isMobile) {
showExtendPage( showExtendPage(
context, context,
@@ -243,7 +246,8 @@ class ListItem<T> extends StatelessWidget {
final nextDelegate = delegate as NextDelegate; final nextDelegate = delegate as NextDelegate;
return _buildListTile( return _buildListTile(
onTab: () { onTab: () {
final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) { if (!isMobile) {
showExtendPage( showExtendPage(
context, context,
@@ -319,3 +323,101 @@ class ListItem<T> extends StatelessWidget {
); );
} }
} }
class ListHeader extends StatelessWidget {
final String title;
final List<Widget> actions;
const ListHeader({
super.key,
required this.title,
List<Widget>? actions,
}) : actions = actions ?? const [];
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...actions,
],
),
),
],
),
);
}
}
List<Widget> generateSection({
required String title,
required Iterable<Widget> items,
List<Widget>? actions,
bool separated = true,
}) {
final genItems = separated
? items.separated(
const Divider(
height: 0,
),
)
: items;
return [
if (items.isNotEmpty)
ListHeader(
title: title,
actions: actions,
),
...genItems,
];
}
List<Widget> generateInfoSection({
required Info info,
required Iterable<Widget> items,
List<Widget>? actions,
bool separated = true,
}) {
final genItems = separated
? items.separated(
const Divider(
height: 0,
),
)
: items;
return [
if (items.isNotEmpty)
InfoHeader(
info: info,
actions: actions,
),
...genItems,
];
}
Widget generateListView(List<Widget> items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) => items[index],
);
}

View File

@@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
class CommonScaffold extends StatefulWidget { class CommonScaffold extends StatefulWidget {
final Widget body; final Widget body;
final Widget? bottomNavigationBar; final Widget? bottomNavigationBar;
final Widget? floatingActionButton;
final String title; final String title;
final Widget? leading; final Widget? leading;
final List<Widget>? actions; final List<Widget>? actions;
@@ -20,7 +19,6 @@ class CommonScaffold extends StatefulWidget {
this.leading, this.leading,
required this.title, required this.title,
this.actions, this.actions,
this.floatingActionButton,
this.automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
}); });
@@ -51,8 +49,6 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> { class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]); final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false); final ValueNotifier<bool> _loading = ValueNotifier(false);
set actions(List<Widget> actions) { set actions(List<Widget> actions) {
@@ -61,12 +57,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
} }
} }
set floatingActionButton(Widget? actions) {
if (_floatingActionButton.value != actions) {
_floatingActionButton.value = actions;
}
}
Future<T?> loadingRun<T>( Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, { Future<T> Function() futureFunction, {
String? title, String? title,
@@ -91,7 +81,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override @override
void dispose() { void dispose() {
_actions.dispose(); _actions.dispose();
_floatingActionButton.dispose();
super.dispose(); super.dispose();
} }
@@ -100,7 +89,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) { if (oldWidget.title != widget.title) {
_actions.value = []; _actions.value = [];
_floatingActionButton.value = null;
} }
} }
@@ -125,13 +113,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _platformContainer( return _platformContainer(
child: Scaffold( child: Scaffold(
floatingActionButton: widget.floatingActionButton ??
ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),

View File

@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
class Section extends StatelessWidget {
final String title;
final Widget child;
const Section({
super.key,
required this.title,
required this.child,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
Expanded(
flex: 0,
child: child,
)
],
);
}
}

View File

@@ -22,5 +22,4 @@ export 'tile_container.dart';
export 'chip.dart'; export 'chip.dart';
export 'fade_box.dart'; export 'fade_box.dart';
export 'app_state_container.dart'; export 'app_state_container.dart';
export 'text.dart'; export 'text.dart';
export 'section.dart';

View File

@@ -1,5 +1,8 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.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:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class WindowContainer extends StatefulWidget { class WindowContainer extends StatefulWidget {
@@ -14,11 +17,22 @@ class WindowContainer extends StatefulWidget {
State<WindowContainer> createState() => _WindowContainerState(); State<WindowContainer> createState() => _WindowContainerState();
} }
class _WindowContainerState extends State<WindowContainer> class _WindowContainerState extends State<WindowContainer> with WindowListener {
with WindowListener {
_autoLaunchContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.autoLaunch,
builder: (_, isAutoLaunch, child) {
autoLaunch?.updateStatus(isAutoLaunch);
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; return _autoLaunchContainer(widget.child);
} }
@override @override
@@ -33,7 +47,6 @@ class _WindowContainerState extends State<WindowContainer>
super.onWindowClose(); super.onWindowClose();
} }
@override @override
void onWindowMinimize() async { void onWindowMinimize() async {
await globalState.appController.savePreferences(); await globalState.appController.savePreferences();

View File

@@ -229,10 +229,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.3+1" version: "5.5.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -277,10 +285,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45" sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.5" version: "8.0.6"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@@ -369,10 +377,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.2"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -630,7 +638,7 @@ packages:
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.16+1"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: "direct main"
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
@@ -1162,7 +1170,7 @@ packages:
source: hosted source: hosted
version: "1.2.2" version: "1.2.2"
win32: win32:
dependency: transitive dependency: "direct main"
description: description:
name: win32 name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.32 version: 0.8.39+202407151
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
@@ -16,7 +16,6 @@ dependencies:
shared_preferences: ^2.2.0 shared_preferences: ^2.2.0
provider: ^6.0.5 provider: ^6.0.5
window_manager: ^0.3.8 window_manager: ^0.3.8
ffi: ^2.1.0
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
proxy: proxy:
path: plugins/proxy path: plugins/proxy
@@ -41,6 +40,9 @@ dependencies:
country_flags: ^2.2.0 country_flags: ^2.2.0
re_editor: ^0.3.0 re_editor: ^0.3.0
re_highlight: ^0.0.3 re_highlight: ^0.0.3
win32: ^5.5.1
ffi: ^2.1.2
material_color_utilities: ^0.8.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@@ -1,67 +1,10 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'package:fl_clash/common/common.dart';
void main() async { void main() async {
String input = """
<details markdown=1><summary>All changes from v0.8.5 to the latest commit:</summary>
print("https://pqjc.site:10000/test.ymal".isUrl);
(unreleased) print("abcd".isUrl);
------------ print("http://10.31.1.221:8848/cfa.yaml".isUrl);
- Fix submit error. [chen08209]
- Add WebDAV. [chen08209]
add Auto check updates
Optimize more details
- Optimize delayTest. [chen08209]
- Upgrade flutter version. [chen08209]
- Update kernel Add import profile via QR code image. [chen08209]
- Add compatibility mode and adapt clash scheme. [chen08209]
- Update Version. [chen08209]
- Reconstruction application proxy logic. [chen08209]
- Fix Tab destroy error. [chen08209]
- Optimize repeat healthcheck. [chen08209]
- Optimize Direct mode ui. [chen08209]
- Optimize Healthcheck. [chen08209]
- Remove proxies position animation, improve performance Add Telegram
Link. [chen08209]
- Update healthcheck policy. [chen08209]
- New Check URLTest. [chen08209]
- Fix the problem of invalid auto-selection. [chen08209]
- New Async UpdateConfig. [chen08209]
- Add changeProfileDebounce. [chen08209]
- Update Workflow. [chen08209]
- Fix ChangeProfile block. [chen08209]
- Fix Release Message Error. [chen08209]
- Update Selector 2. [chen08209]
- Update Version. [chen08209]
- Fix Proxies Select Error. [chen08209]
- Fix the problem that the proxy group is empty in global mode.
[chen08209]
- Fix the problem that the proxy group is empty in global mode.
[chen08209]
- Add ProxyProvider2. [chen08209]
- Add ProxyProvider. [chen08209]
- Update Version. [chen08209]
- Update ProxyGroup Sort. [chen08209]
- Fix Android quickStart VpnService some problems. [chen08209]
- Update version. [chen08209]
- Set Android notification low importance. [chen08209]
- Fix the issue that VpnService can't be closed correctly in special
cases. [chen08209]
- Fix the problem that TileService is not destroyed correctly in some
cases. [chen08209]
Adjust tab animation defaults
- Add Telegram in README_zh_CN.md. [chen08209]
- Add Telegram. [chen08209]
""";
const pattern = r'- (.+?)\. \[.+?\]';
final regex = RegExp(pattern);
for (final match in regex.allMatches(input)) {
final change = match.group(1);
print(change);
}
} }

View File

@@ -17,6 +17,11 @@ add_executable(${BINARY_NAME} WIN32
"Runner.rc" "Runner.rc"
"runner.exe.manifest" "runner.exe.manifest"
) )
# add_executable(service
# "service.cpp"
# )
# Apply the standard set of build settings. This can be removed for applications # Apply the standard set of build settings. This can be removed for applications
# that need different build settings. # that need different build settings.
apply_standard_settings(${BINARY_NAME}) apply_standard_settings(${BINARY_NAME})

152
windows/runner/service.cpp Normal file
View File

@@ -0,0 +1,152 @@
#include <windows.h>
#include <tchar.h>
#include <string>
#define SERVICE_NAME _T("MyService")
SERVICE_STATUS g_ServiceStatus = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv);
VOID WINAPI ServiceCtrlHandler(DWORD);
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
int _tmain(int argc, TCHAR *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] =
{
{SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
{NULL, NULL}
};
if (StartServiceCtrlDispatcher(ServiceTable) == FALSE)
{
return GetLastError();
}
return 0;
}
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv)
{
DWORD Status = E_FAIL;
g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, ServiceCtrlHandler);
if (g_StatusHandle == NULL)
{
goto EXIT;
}
ZeroMemory(&g_ServiceStatus, sizeof(g_ServiceStatus));
g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwServiceSpecificExitCode = 0;
g_ServiceStatus.dwCheckPoint = 0;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
g_ServiceStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (g_ServiceStopEvent == NULL)
{
g_ServiceStatus.dwControlsAccepted = 0;
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwWin32ExitCode = GetLastError();
g_ServiceStatus.dwCheckPoint = 1;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
goto EXIT;
}
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
g_ServiceStatus.dwCheckPoint = 0;
g_ServiceStatus.dwWaitHint = 0;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
HANDLE hThread = CreateThread(NULL, 0, ServiceWorkerThread, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(g_ServiceStopEvent);
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwCheckPoint = 3;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceMain: SetServiceStatus returned error"));
}
EXIT:
return;
}
VOID WINAPI ServiceCtrlHandler(DWORD CtrlCode)
{
switch(CtrlCode)
{
case SERVICE_CONTROL_STOP:
if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING)
break;
g_ServiceStatus.dwControlsAccepted = 0;
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
g_ServiceStatus.dwWin32ExitCode = 0;
g_ServiceStatus.dwCheckPoint = 4;
if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
{
OutputDebugString(_T("My Service: ServiceCtrlHandler: SetServiceStatus returned error"));
}
SetEvent(g_ServiceStopEvent);
break;
default:
break;
}
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
{
while(WaitForSingleObject(g_ServiceStopEvent, 0) != WAIT_OBJECT_0)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// 启动 "C:\path\to\your\executable.exe"
if(!CreateProcess(NULL, _T("C:\\path\\to\\your\\executable.exe"), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
OutputDebugString(_T("CreateProcess failed"));
}
// 等待进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
// 关闭进程和线程句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
// 每隔一段时间检查一次这里设置为60秒
Sleep(60000);
}
return ERROR_SUCCESS;
}