Compare commits

..

20 Commits

Author SHA1 Message Date
chen08209
ba91fab2b5 Fix autoRun show issues
Fix Android 10 issues

Optimize ip show
2024-06-23 03:06:28 +08:00
chen08209
18add7fba3 Add intranet IP display
Add connections page

Add search in connections, requests

Add keyword search in connections, requests, logs

Add basic viewing editing capabilities

Optimize update profile
2024-06-22 03:01:07 +08:00
chen08209
0d3034f216 Update version
(cherry picked from commit afa1b4f424)
2024-06-19 13:13:16 +08:00
chen08209
313faa8cf3 Fix the problem of excessive memory usage in traffic usage.
Add lightBlue theme color

Fix start unable to update profile issues
2024-06-19 10:09:57 +08:00
chen08209
4ba2f72b1a Fix flashback caused by process 2024-06-17 15:46:06 +08:00
chen08209
d6c22d4cca Add build version
Optimize quick start

Update system default option

(cherry picked from commit 05abf2d56d)
2024-06-16 18:54:22 +08:00
chen08209
614dfc841a Update build.yml
(cherry picked from commit 658727dd79)
2024-06-16 18:54:20 +08:00
chen08209
5afa552b6c Fix android vpn close issues
Add requests page

Fix checkUpdate dark mode style error

Fix quickStart error open app

Add memory proxies tab index

Support hidden group

Optimize logs
2024-06-16 13:05:46 +08:00
chen08209
ee0e2a15a7 Fix externalController hot load error 2024-06-13 19:21:45 +08:00
chen08209
7b7fd084bf Add tcp concurrent switch
Add system proxy switch

Add geodata loader switch

Add external controller switch

Add auto gc on trim memory

Fix android notification error
2024-06-13 17:03:51 +08:00
chen08209
03c39bf4bf Fix ipv6 error
(cherry picked from commit 86572cc960)
2024-06-12 19:10:04 +08:00
chen08209
fd7c3be02f Fix android udp direct error
Add ipv6 switch

Add access all selected button

Remove android low version splash
2024-06-12 18:27:53 +08:00
chen08209
b67d221063 Fix search issues 2024-06-10 19:08:24 +08:00
chen08209
1a74c0a12f Fix LoadBalance, Relay load error
(cherry picked from commit 7acf9c6db3)
2024-06-10 19:07:28 +08:00
chen08209
9fc9c53427 Add allowBypass
Fix Android only pick .text file issues
2024-06-10 19:07:04 +08:00
chen08209
8954b88d7f Fix build.yml4 2024-06-09 19:55:59 +08:00
chen08209
3990f8d7fa Fix build.yml3
(cherry picked from commit 8a01e04871)
2024-06-09 19:53:02 +08:00
chen08209
98688bcd2a Fix build.yml2
(cherry picked from commit 7ddcdd9828)
2024-06-09 19:53:02 +08:00
chen08209
2957311682 Fix build.yml
(cherry picked from commit d89ed076fd)
2024-06-09 19:53:01 +08:00
chen08209
87a903af60 Add search function at access control
Fix the issues with the profile add button to cover the edit button

Adapt LoadBalance and Relay

Add arm

Fix android notification icon error
2024-06-09 19:22:49 +08:00
105 changed files with 2744 additions and 9694 deletions

View File

@@ -21,25 +21,11 @@ jobs:
os: macos-13
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
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
@@ -66,10 +52,10 @@ jobs:
if: startsWith(matrix.platform,'android')
run: |
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 "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go
uses: actions/setup-go@v5

View File

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

View File

@@ -1,15 +1,10 @@
package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData
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
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import java.util.Date
enum class RunState {
START,
@@ -17,46 +12,16 @@ enum class RunState {
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?
object GlobalState {
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,
)
}
fun getCurrentAppPlugin(): AppPlugin? =
flutterEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
@@ -11,9 +11,14 @@ import androidx.lifecycle.Observer
import com.follow.clash.GlobalState
import com.follow.clash.RunState
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() {
private val observer = Observer<RunState> { runState ->
@@ -57,25 +62,42 @@ class FlClashTileService : TileService() {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
} else {
}else{
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() {
super.onClick()
activityTransfer()
val currentTilePlugin = GlobalState.getCurrentTilePlugin()
if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
val titlePlugin = GlobalState.getCurrentTitlePlugin()
if (titlePlugin != null) {
titlePlugin.handleStart()
} else {
GlobalState.initServiceEngine(applicationContext)
if(currentTilePlugin == null){
initFlutterEngine()
}else{
currentTilePlugin.handleStart()
}
} else if (GlobalState.runState.value == RunState.START) {
} else if(GlobalState.runState.value == RunState.START){
GlobalState.runState.value = RunState.PENDING
GlobalState.getCurrentTitlePlugin()?.handleStop()
currentTilePlugin?.handleStop()
}
}

View File

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

View File

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

Binary file not shown.

View File

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

View File

@@ -1,7 +1,6 @@
package dart_bridge
/*
#include <stdlib.h>
#include "stdint.h"
#include "include/dart_api_dl.h"
#include "include/dart_api_dl.c"
@@ -25,16 +24,14 @@ func InitDartApi(api unsafe.Pointer) {
}
}
func SendToPort(port int64, msg string) bool {
func SendToPort(port int64, msg string) {
var obj C.Dart_CObject
obj._type = C.Dart_CObject_kString
msgString := C.CString(msg)
defer C.free(unsafe.Pointer(msgString))
ptr := unsafe.Pointer(&obj.value[0])
*(**C.char)(ptr) = msgString
isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj)
if !isSuccess {
return false
fmt.Println("ERROR: post to port ", port, " failed", msg)
}
return true
}

View File

@@ -0,0 +1,38 @@
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

@@ -1,8 +1,5 @@
package main
/*
#include <stdlib.h>
*/
import "C"
import (
bridge "core/dart-bridge"
@@ -13,7 +10,6 @@ import (
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
@@ -32,12 +28,8 @@ import (
var currentConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
var isInit = false
var currentProfileName = ""
//export initClash
func initClash(homeDirStr *C.char) bool {
if !isInit {
@@ -76,21 +68,11 @@ 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
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
bytes := []byte(C.GoString(s))
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
@@ -103,18 +85,21 @@ func validateConfig(s *C.char, port C.longlong) {
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
paramsString := C.GoString(s)
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
configParams = params.Params
prof := decorationConfig(params.ProfilePath, params.Config)
prof := decorationConfig(params.ProfilePath, *params.Config, *params.IsCompatible)
currentConfig = prof
applyConfig()
if *params.IsPatch {
applyConfig(true)
} else {
applyConfig(false)
}
bridge.SendToPort(i, "")
}()
}
@@ -162,32 +147,30 @@ func getProxies() *C.char {
}
//export changeProxy
func changeProxy(s *C.char) {
paramsString := C.GoString(s)
func changeProxy(s *C.char) bool {
go func() {
paramsString := C.GoString(s)
var params = &ChangeProxyParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
log.Infoln("Unmarshal ChangeProxyParams %v", err)
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
group, ok := proxies[groupName]
if !ok {
proxy := proxies[*params.GroupName]
if proxy == nil {
return
}
adapterProxy := group.(*adapter.Proxy)
log.Infoln("change proxy %s", proxy.Name())
adapterProxy := proxy.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector)
if !ok {
return
}
err = selector.Set(proxyName)
if err == nil {
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
if err := selector.Set(*params.ProxyName); err != nil {
return
}
}()
return true
}
//export getTraffic
@@ -228,8 +211,8 @@ func resetTraffic() {
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
paramsString := C.GoString(s)
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
@@ -313,6 +296,7 @@ func closeConnections() bool {
//export closeConnection
func closeConnection(id *C.char) bool {
connectionId := C.GoString(id)
err := statistic.DefaultManager.Get(connectionId).Close()
if err != nil {
return false
@@ -323,13 +307,10 @@ func closeConnection(id *C.char) bool {
//export getProviders
func getProviders() *C.char {
data, err := json.Marshal(tunnel.Providers())
var msg *C.char
if err != nil {
msg = C.CString("")
return msg
return C.CString("")
}
msg = C.CString(string(data))
return msg
return C.CString(string(data))
}
//export getProvider
@@ -379,9 +360,10 @@ func getExternalProviders() *C.char {
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
i := int64(port)
providerNameString := C.GoString(providerName)
providerTypeString := C.GoString(providerType)
go func() {
providerNameString := C.GoString(providerName)
providerTypeString := C.GoString(providerType)
switch providerTypeString {
case "Proxy":
providers := tunnel.Providers()
@@ -397,49 +379,34 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
bridge.SendToPort(i, err.Error())
return
}
case "MMDB":
case "GeoIp":
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := mmdb.DownloadGeoSite(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
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, "")
}()
}
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
func initNativeApiBridge(api unsafe.Pointer, port C.longlong) {
bridge.InitDartApi(api)
}
//export initMessage
func initMessage(port C.longlong) {
i := int64(port)
Port = i
}
//export freeCString
func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
bridge.Port = &i
}
func init() {
@@ -452,21 +419,15 @@ func init() {
} else {
delayData.Value = int32(delay)
}
SendMessage(Message{
Type: DelayMessage,
bridge.SendMessage(bridge.Message{
Type: bridge.Delay,
Data: delayData,
})
}
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
SendMessage(Message{
Type: RequestMessage,
bridge.SendMessage(bridge.Message{
Type: bridge.Request,
Data: c,
})
}
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
SendMessage(Message{
Type: LoadedMessage,
Data: providerName,
})
}
}

View File

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

View File

@@ -1,77 +0,0 @@
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

@@ -1,42 +0,0 @@
//go:build android
package platform
import "syscall"
var nullFd int
var maxFdCount int
func init() {
fd, err := syscall.Open("/dev/null", syscall.O_WRONLY, 0644)
if err != nil {
panic(err.Error())
}
nullFd = fd
var limit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
maxFdCount = 1024
} else {
maxFdCount = int(limit.Cur)
}
maxFdCount = maxFdCount / 4 * 3
}
func ShouldBlockConnection() bool {
fd, err := syscall.Dup(nullFd)
if err != nil {
return true
}
_ = syscall.Close(fd)
if fd > maxFdCount {
return true
}
return false
}

View File

@@ -4,6 +4,7 @@ package main
import "C"
import (
bridge "core/dart-bridge"
"encoding/json"
"errors"
"github.com/metacubex/mihomo/component/process"
@@ -42,8 +43,8 @@ func init() {
timeout := time.After(200 * time.Millisecond)
SendMessage(Message{
Type: ProcessMessage,
bridge.SendMessage(bridge.Message{
Type: bridge.Process,
Data: Process{
Id: id,
Metadata: metadata,
@@ -70,8 +71,8 @@ func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
go func() {
paramsString := C.GoString(s)
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {

View File

@@ -4,42 +4,20 @@ package main
import "C"
import (
"core/platform"
t "core/tun"
"errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
)
var tunLock sync.Mutex
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
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
func startTUN(fd C.int) {
go func() {
tunLock.Lock()
defer tunLock.Unlock()
@@ -48,7 +26,6 @@ func startTUN(fd C.int, port C.longlong) {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
@@ -66,34 +43,15 @@ func startTUN(fd C.int, port C.longlong) {
tempTun.Closer = closer
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
func stopTun() {
go func() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tun != nil {
tun.Close()
tun = nil
@@ -101,60 +59,12 @@ func stopTun() {
}()
}
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() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
if tun == nil {
return
}
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)
}
if tun != nil {
tun.MarkSocket(int(fd))
time.Sleep(time.Millisecond * 100)
}
})
}

View File

@@ -5,6 +5,7 @@ package tun
import "C"
import (
"context"
bridge "core/dart-bridge"
"encoding/binary"
"github.com/Kr328/tun2socket"
"github.com/Kr328/tun2socket/nat"
@@ -18,6 +19,7 @@ import (
"io"
"net"
"os"
"strconv"
"time"
)
@@ -184,3 +186,19 @@ func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
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,6 +115,7 @@ class ApplicationState extends State<Application> {
lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
});
@@ -129,6 +130,7 @@ class ApplicationState extends State<Application> {
httpTimeoutDuration,
(timer) async {
await globalState.appController.updateGroups();
globalState.appController.appState.sortNum++;
},
);
}
@@ -163,6 +165,7 @@ class ApplicationState extends State<Application> {
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
@@ -172,6 +175,7 @@ class ApplicationState extends State<Application> {
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,

View File

@@ -35,6 +35,7 @@ class ClashCore {
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
receiver.sendPort.nativePort,
);
}
@@ -44,10 +45,10 @@ class ClashCore {
}
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
return clashFFI.initClash(
homeDir.toNativeUtf8().cast(),
) ==
1;
}
shutdown() {
@@ -66,12 +67,10 @@ class ClashCore {
receiver.close();
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
data.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
}
@@ -85,45 +84,20 @@ class ClashCore {
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
params.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
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() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
if (proxies.isEmpty) return [];
if(proxiesRawString.isEmpty) return [];
final proxies = json.decode(proxiesRawString) as Map;
if(proxies.isEmpty) return [];
final groupNames = [
UsedProxy.GLOBAL.name,
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
@@ -137,8 +111,7 @@ class ClashCore {
group["all"] = ((group["all"] ?? []) as List)
.map(
(name) => proxies[name],
)
.where((proxy) => proxy != null)
)
.toList();
return group;
}).toList();
@@ -149,15 +122,14 @@ class ClashCore {
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
externalProvidersRaw.cast<Utf8>().toDartString();
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
}
@@ -174,23 +146,17 @@ class ClashCore {
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final providerTypeChar = providerType.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
providerTypeChar,
providerName.toNativeUtf8().cast(),
providerType.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(providerTypeChar);
return completer.future;
}
changeProxy(ChangeProxyParams changeProxyParams) {
bool changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar);
return clashFFI.changeProxy(params.toNativeUtf8().cast()) == 1;
}
Future<Delay> getDelay(String proxyName) {
@@ -206,16 +172,13 @@ class ClashCore {
receiver.close();
}
});
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
json.encode(delayParams).toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
Future.delayed(httpTimeoutDuration + moreDuration, () {
receiver.close();
if (!completer.isCompleted) {
if(!completer.isCompleted){
completer.complete(
Delay(name: proxyName, value: -1),
);
@@ -225,33 +188,28 @@ class ClashCore {
}
clearEffect(String path) {
final pathChar = path.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(pathChar);
malloc.free(pathChar);
clashFFI.clearEffect(path.toNativeUtf8().cast());
}
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(versionInfoRaw);
return VersionInfo.fromJson(versionInfo);
}
Traffic getTraffic() {
final trafficRaw = clashFFI.getTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
Traffic getTotalTraffic() {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
void resetTraffic() {
void resetTraffic(){
clashFFI.resetTraffic();
}
@@ -263,9 +221,8 @@ class ClashCore {
clashFFI.stopLog();
}
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
startTun(int fd) {
clashFFI.startTUN(fd);
}
requestGc() {
@@ -277,37 +234,25 @@ class ClashCore {
}
void setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
clashFFI.setProcessMap(json.encode(processMapItem).toNativeUtf8().cast());
}
void setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
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));
}
// DateTime? getRunTime() {
// final runTimeString = clashFFI.getRunTime().cast<Utf8>().toDartString();
// if (runTimeString.isEmpty) return null;
// return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
// }
List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsData =
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
clashFFI.freeCString(connectionsDataRaw);
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnections(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
clashFFI.closeConnection(id.toNativeUtf8().cast());
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import 'dart:ui';
import 'package:fl_clash/models/clash_config.dart';
import 'package:flutter/material.dart';
const appName = "FlClash";
@@ -8,18 +7,10 @@ const coreName = "clash.meta";
const packageName = "FlClash";
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb";
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 asnFileName = "ASN.mmdb";
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
@@ -32,7 +23,6 @@ const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,

View File

@@ -7,12 +7,8 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<CommonScaffoldState>();
}
Size get appSize{
return MediaQuery.of(this).size;
}
double get width {
return appSize.width;
return MediaQuery.of(this).size.width;
}
ColorScheme get colorScheme => Theme.of(this).colorScheme;

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,20 @@ class ProxyManager {
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() {
_instance ??= ProxyManager._internal();
return _instance!;

View File

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

View File

@@ -1,110 +0,0 @@
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,5 +1,10 @@
extension StringExtension on String {
bool get isUrl {
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
try {
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 toMinus => copyWith(fontSize: fontSize! - 2);
TextStyle get toMinus => copyWith(fontSize: fontSize! - 1);
}

View File

@@ -17,7 +17,6 @@ class AppController {
late ClashConfig clashConfig;
late Measure measure;
late Function updateClashConfigDebounce;
late Function addCheckIpNumDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -26,9 +25,6 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
measure = Measure.of(context);
}
@@ -70,10 +66,19 @@ class AppController {
updateTraffic() {
globalState.updateTraffic(
config: config,
appState: appState,
);
}
changeProxy() {
globalState.changeProxy(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
addProfile(Profile profile) async {
config.setProfile(profile);
if (config.currentProfileId != null) return;
@@ -120,14 +125,6 @@ class AppController {
});
}
Future rawApplyProfile() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
changeProfile(String? value) async {
if (value == config.currentProfileId) return;
config.currentProfileId = value;
@@ -362,9 +359,7 @@ class AppController {
}
addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
}
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
@@ -410,54 +405,9 @@ class AppController {
addProfileFormURL(url);
}
int get columns =>
globalState.getColumns(appState.viewMode, config.proxiesColumns);
changeColumns() {
config.proxiesColumns = globalState.getColumns(
appState.viewMode,
columns - 1,
);
}
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
});
}
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(a.name, b.name),
);
}
List<Proxy> _sortOfDelay(List<Proxy> proxies) {
return proxies = List.of(proxies)
..sort(
(a, b) {
final aDelay = appState.getDelay(a.name);
final bDelay = appState.getDelay(b.name);
if (aDelay == null && bDelay == null) {
return 0;
}
if (aDelay == null || aDelay == -1) {
return 1;
}
if (bDelay == null || bDelay == -1) {
return -1;
}
return aDelay.compareTo(bDelay);
},
);
}
List<Proxy> getSortProxies(List<Proxy> proxies) {
return switch (config.proxiesSortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(proxies),
ProxiesSortType.name => _sortOfName(proxies),
};
}
}

View File

@@ -56,20 +56,7 @@ enum ProfileType { file, url }
enum ResultType { success, error }
enum AppMessageType {
log,
delay,
request,
started,
loaded,
}
enum ServiceMessageType {
protect,
process,
started,
loaded,
}
enum MessageType { log, tun, delay, process, now, request, run }
enum FindProcessMode { always, off }
@@ -78,10 +65,7 @@ enum RecoveryOption {
onlyProfiles,
}
enum ChipType { action, delete }
enum CommonCardType { plain, filled }
enum ProxiesType { tab, expansion }
enum ProxyCardType { expand, shrink }
enum ChipType {
action,
delete,
}

View File

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

View File

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

View File

@@ -89,24 +89,6 @@ 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)
Selector<Config, bool>(
selector: (_, config) => config.isAnimateToPage,

View File

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

View File

@@ -39,6 +39,81 @@ 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) {
globalState.showCommonDialog(
child: AlertDialog(
@@ -75,364 +150,238 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
}
_showUaDialog(String? value) {
const uas = [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
];
globalState.showCommonDialog(
child: AlertDialog(
title: const Text("UA"),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
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),
)
],
),
),
),
);
}
_modifyTestUrl(String testUrl) async {
final newTestUrl = await globalState.showCommonDialog<String>(
child: TestUrlFormDialog(
testUrl: testUrl,
),
);
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;
},
),
);
Widget _buildGeneralSection() {
final 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);
},
),
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<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTab: () {
_modifyMixedPort(mixedPort);
},
),
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.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: () {
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_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",
),
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(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
Widget _buildMoreSection() {
final items = [
if (false)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
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();
},
leading: const Icon(
Icons.important_devices_outlined
),
);
},
),
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),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: allowLan,
value: tunEnable,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
clashConfig.tun = Tun(enable: 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();
},
),
);
},
),
],
);
}
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();
},
),
);
},
),
],
];
if(items.isEmpty) return Container();
return Section(
title: appLocalizations.general,
child: Column(
children: [
for (final item in items) ...[
item,
if (items.last != item)
const Divider(
height: 0,
)
]
],
),
);
}
@override
Widget build(BuildContext context) {
List<Widget> items = [
..._buildAppSection(),
..._buildGeneralSection(),
..._buildMoreSection(),
_buildAppSection(),
_buildGeneralSection(),
_buildMoreSection(),
];
return ListView.builder(
padding: const EdgeInsets.only(bottom: 32),
@@ -465,7 +414,7 @@ class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
portController = TextEditingController(text: "${widget.mixedPort}");
}
_handleUpdate() async {
_handleAddProfileFormURL() async {
final port = portController.value.text;
if (port.isEmpty) return;
Navigator.of(context).pop<String>(port);
@@ -491,64 +440,7 @@ class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
),
actions: [
TextButton(
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,
onPressed: _handleAddProfileFormURL,
child: Text(appLocalizations.submit),
)
],

View File

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

View File

@@ -13,7 +13,6 @@ class CoreInfo extends StatelessWidget {
selector: (_, appState) => appState.versionInfo,
builder: (_, versionInfo, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.coreInfo,
iconData: Icons.memory,

View File

@@ -56,7 +56,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const IntranetIP(),
child: const IntranetIp(),
),
],
);

View File

@@ -5,14 +5,14 @@ import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class IntranetIP extends StatefulWidget {
const IntranetIP({super.key});
class IntranetIp extends StatefulWidget {
const IntranetIp({super.key});
@override
State<IntranetIP> createState() => _IntranetIPState();
State<IntranetIp> createState() => _IntranetIpState();
}
class _IntranetIPState extends State<IntranetIP> {
class _IntranetIpState extends State<IntranetIp> {
final ipNotifier = ValueNotifier<String>("");
Future<String?> getLocalIpAddress() async {
@@ -45,15 +45,12 @@ class _IntranetIPState extends State<IntranetIP> {
Widget build(BuildContext context) {
return CommonCard(
info: Info(
label: appLocalizations.intranetIP,
label: appLocalizations.intranetIp,
iconData: Icons.devices,
),
onPressed: (){
},
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 24 - 2,
height: globalState.appController.measure.titleLargeHeight + 24 - 1,
child: ValueListenableBuilder(
valueListenable: ipNotifier,
builder: (_, value, __) {

View File

@@ -52,7 +52,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
isInit: appState.isInit,
selectedMap: appState.selectedMap,
isStart: appState.isStart,
checkIpNum: appState.checkIpNum,
);
},
builder: (_, state, __) {
@@ -79,7 +78,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
valueListenable: ipInfoNotifier,
builder: (_, ipInfo, __) {
return CommonCard(
onPressed: () {},
child: Column(
children: [
Flexible(
@@ -136,9 +134,8 @@ class _NetworkDetectionState extends State<NetworkDetection> {
),
),
Container(
height: globalState.appController.measure.titleLargeHeight +
24 -
2,
height:
globalState.appController.measure.titleLargeHeight + 24 - 1,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox(
@@ -169,8 +166,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
"timeout",
style: context.textTheme.titleLarge
?.copyWith(color: Colors.red)
.toSoftBold
.toMinus,
.toSoftBold.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);

View File

@@ -111,7 +111,6 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed,

View File

@@ -13,10 +13,18 @@ class OutboundMode extends StatelessWidget {
_changeMode(BuildContext context, Mode? value) async {
final appController = globalState.appController;
final clashConfig = appController.clashConfig;
final config = appController.config;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
await appController.updateClashConfig();
appController.addCheckIpNumDebounce();
if (!config.isCompatible) {
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
@@ -25,7 +33,6 @@ class OutboundMode extends StatelessWidget {
selector: (_, clashConfig) => clashConfig.mode,
builder: (_, mode, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split,
@@ -56,8 +63,11 @@ class OutboundMode extends StatelessWidget {
),
title: Text(
Intl.message(item.name),
style:
Theme.of(context).textTheme.titleMedium?.toSoftBold,
style: Theme
.of(context)
.textTheme
.titleMedium
?.toSoftBold,
),
),
],

View File

@@ -51,7 +51,6 @@ class TrafficUsage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,

View File

@@ -1,4 +1,4 @@
export 'proxies.dart';
export 'proxies/proxies.dart';
export 'dashboard/dashboard.dart';
export 'tools.dart';
export 'profiles/profiles.dart';

View File

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

View File

@@ -25,9 +25,7 @@ class AddProfile extends StatelessWidget {
final url = await Navigator.of(context)
.push<String>(MaterialPageRoute(builder: (_) => const ScanPage()));
if (url != null) {
WidgetsBinding.instance.addPostFrameCallback((_){
_handleAddProfileFormURL(url);
});
_handleAddProfileFormURL(url);
}
}
@@ -93,8 +91,7 @@ class _URLFormDialogState extends State<URLFormDialog> {
runSpacing: 16,
children: [
TextField(
maxLines: 5,
minLines: 1,
maxLines: null,
controller: urlController,
decoration: InputDecoration(
border: const OutlineInputBorder(),

View File

@@ -94,8 +94,7 @@ class _EditProfileState extends State<EditProfile> {
ListItem(
title: TextFormField(
controller: urlController,
maxLines: 5,
minLines: 1,
maxLines: null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: appLocalizations.url,

View File

@@ -28,7 +28,6 @@ class ProfilesFragment extends StatefulWidget {
class _ProfilesFragmentState extends State<ProfilesFragment> {
final hasPadding = ValueNotifier<bool>(false);
Function? applyConfigDebounce;
List<GlobalObjectKey<_ProfileItemState>> profileItemKeys = [];
@@ -56,7 +55,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
_updateProfiles() async {
final updateProfiles = profileItemKeys.map<Future>(
(key) async => await key.currentState?.updateProfile(false));
await Future.wait(updateProfiles);
final result = await Future.wait(updateProfiles);
}
_initScaffoldState() {
@@ -76,116 +75,97 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
width: 8,
)
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
},
);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.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
Widget build(BuildContext context) {
return FloatLayout(
floatingWidget: FloatWrapper(
child: FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
),
),
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
return child!;
},
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,
),
),
],
),
);
},
),
),
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);
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,
),
),
],
),
);
},
),
),
);
},
),
);
}
@@ -221,18 +201,11 @@ class _ProfileItemState extends State<ProfileItem> {
Future updateProfile([isSingle = true]) async {
isUpdating.value = true;
try {
final appController = globalState.appController;
await appController.updateProfile(widget.profile);
if (widget.profile.id == appController.config.currentProfile?.id &&
!appController.appState.isStart) {
globalState.appController.rawApplyProfile();
}
await globalState.appController.updateProfile(widget.profile);
} catch (e) {
isUpdating.value = false;
if (!isSingle) {
return e.toString();
} else {
rethrow;
}
}
isUpdating.value = false;
@@ -420,7 +393,16 @@ class _ProfileItemState extends State<ProfileItem> {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
return CommonCard(
return Selector<AppState, ViewMode>(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, child) {
if (viewMode == ViewMode.mobile) {
return child!;
}
return CommonCard(
child: child!,
);
},
child: ListItem.radio(
key: Key(profile.id),
horizontalTitleGap: 16,
@@ -455,16 +437,16 @@ class _ProfileItemState extends State<ProfileItem> {
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.view,
label: appLocalizations.view,
iconData: Icons.visibility,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
CommonPopupMenuItem(
action: ProfileActions.view,
label: "查看",
iconData: Icons.visibility,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {

View File

@@ -1,824 +0,0 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/clash/core.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:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProxiesFragment extends StatefulWidget {
const ProxiesFragment({super.key});
@override
State<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
final items = [
CommonPopupMenuItem(
action: ProxiesSortType.none,
label: appLocalizations.defaultSort,
iconData: Icons.reorder,
),
CommonPopupMenuItem(
action: ProxiesSortType.delay,
label: appLocalizations.delaySort,
iconData: Icons.network_ping,
),
CommonPopupMenuItem(
action: ProxiesSortType.name,
label: appLocalizations.nameSort,
iconData: Icons.sort_by_alpha,
),
];
commonScaffoldState?.actions = [
Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return IconButton(
icon: Icon(
switch (proxiesType) {
ProxiesType.tab => Icons.view_list,
ProxiesType.expansion => Icons.view_carousel,
},
),
onPressed: () {
final config = globalState.appController.config;
config.proxiesType = config.proxiesType == ProxiesType.tab
? ProxiesType.expansion
: ProxiesType.tab;
},
);
},
),
IconButton(
icon: const Icon(
Icons.view_column,
),
onPressed: () {
globalState.appController.changeColumns();
},
),
IconButton(
icon: const Icon(Icons.transform_sharp),
onPressed: () {
final config = globalState.appController.config;
config.proxyCardType = config.proxyCardType == ProxyCardType.expand
? ProxyCardType.shrink
: ProxyCardType.expand;
},
),
Selector<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesSortType,
builder: (_, proxiesSortType, __) {
return CommonPopupMenu<ProxiesSortType>.radio(
items: items,
icon: const Icon(Icons.sort_sharp),
onSelected: (value) {
final config = context.read<Config>();
config.proxiesSortType = value;
},
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return switch (proxiesType) {
ProxiesType.tab => const ProxiesTabFragment(),
ProxiesType.expansion => const ProxiesExpansionPanelFragment(),
};
},
),
);
}
}
class ProxiesTabFragment extends StatefulWidget {
const ProxiesTabFragment({super.key});
@override
State<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
}
class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
with TickerProviderStateMixin {
TabController? _tabController;
_handleTabControllerChange() {
final indexIsChanging = _tabController?.indexIsChanging ?? false;
if (indexIsChanging) return;
final index = _tabController?.index;
if (index == null) return;
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(currentGroups[index].name);
}
}
@override
void dispose() {
super.dispose();
_tabController?.dispose();
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_tabController?.removeListener(_handleTabControllerChange);
_tabController?.dispose();
_tabController = null;
return true;
}
return false;
},
builder: (_, state, __) {
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
)..addListener(_handleTabControllerChange);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
padding: const EdgeInsets.symmetric(horizontal: 16),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxyGroupView(
groupName: groupName,
type: ProxiesType.tab,
),
),
],
),
)
],
);
},
);
}
}
class ProxiesExpansionPanelFragment extends StatefulWidget {
const ProxiesExpansionPanelFragment({super.key});
@override
State<ProxiesExpansionPanelFragment> createState() =>
_ProxiesExpansionPanelFragmentState();
}
class _ProxiesExpansionPanelFragmentState
extends State<ProxiesExpansionPanelFragment> {
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: state.groupNames.length,
itemBuilder: (_, index) {
final groupName = state.groupNames[index];
return ProxyGroupView(
key: PageStorageKey(groupName),
groupName: groupName,
type: ProxiesType.expansion,
);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 16,
);
},
);
},
);
}
}
class ProxyGroupView extends StatefulWidget {
final String groupName;
final ProxiesType type;
const ProxyGroupView({
super.key,
required this.groupName,
required this.type,
});
@override
State<ProxyGroupView> createState() => _ProxyGroupViewState();
}
class _ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final scrollController = ScrollController();
var isEnd = false;
String get groupName => widget.groupName;
ProxiesType get type => widget.type;
double _getItemHeight(ProxyCardType proxyCardType) {
final isExpand = proxyCardType == ProxyCardType.expand;
final measure = globalState.appController.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
return isExpand ? baseHeight + measure.labelSmallHeight + 8 : baseHeight;
}
_delayTest(List<Proxy> proxies) async {
if (isLock) return;
isLock = true;
final appController = globalState.appController;
for (final proxy in proxies) {
final proxyName =
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(
name: proxyName,
value: 0,
),
);
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
appController.appState.sortNum++;
isLock = false;
}
Widget _currentProxyNameBuilder({
required Widget Function(String) builder,
}) {
return Selector2<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return config.currentSelectedMap[groupName] ?? group.now ?? '';
},
builder: (_, value, ___) {
return builder(value);
},
);
}
Widget _buildTabGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
child: Align(
alignment: Alignment.topCenter,
child: GridView.builder(
padding: const EdgeInsets.all(16),
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(
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
isSelected: value == proxy.name,
proxy: proxy,
groupName: groupName,
);
});
},
),
),
);
}
Widget _buildExpansionGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
final itemHeight = _getItemHeight(proxyCardType);
final innerHeight = context.appSize.height - 200;
final lines = (sortedProxies.length / columns).ceil();
final minLines =
innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3;
final height = (itemHeight + 8) * min(lines, minLines) - 8;
return Selector<Config, Set<String>>(
selector: (_, config) => config.currentUnfoldSet,
builder: (_, currentUnfoldSet, __) {
return CommonCard(
child: ExpansionTile(
childrenPadding: const EdgeInsets.all(8),
initiallyExpanded: currentUnfoldSet.contains(groupName),
iconColor: context.colorScheme.onSurfaceVariant,
onExpansionChanged: (value) {
final tempUnfoldSet = Set<String>.from(currentUnfoldSet);
if (value) {
tempUnfoldSet.add(groupName);
} else {
tempUnfoldSet.remove(groupName);
}
globalState.appController.config.updateCurrentUnfoldSet(
tempUnfoldSet,
);
},
controlAffinity: ListTileControlAffinity.trailing,
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(groupName),
const SizedBox(
height: 4,
),
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
group.type.name,
style: context.textTheme.labelMedium?.toLight,
),
Flexible(
flex: 1,
child: _currentProxyNameBuilder(
builder: (value) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
if (value.isNotEmpty) ...[
Icon(
Icons.arrow_right,
color: context
.colorScheme.onSurfaceVariant,
),
Flexible(
flex: 1,
child: Text(
overflow: TextOverflow.ellipsis,
value,
style: context
.textTheme.labelMedium?.toLight,
),
),
]
],
);
},
),
),
],
),
),
const SizedBox(
height: 4,
),
],
),
),
IconButton(
icon: Icon(
Icons.network_ping,
size: 20,
color: context.colorScheme.onSurfaceVariant,
),
onPressed: () {
_delayTest(sortedProxies);
},
),
],
),
shape: const RoundedRectangleBorder(
side: BorderSide.none,
),
collapsedShape: const RoundedRectangleBorder(
side: BorderSide.none,
),
children: [
SizedBox(
height: height,
child: GridView.builder(
key: widget.key,
controller: scrollController,
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,
);
},
);
},
),
),
],
),
);
},
);
}
@override
void dispose() {
super.dispose();
scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
sortNum: appState.sortNum,
proxies: group.all,
);
},
builder: (_, state, __) {
final proxies = state.proxies;
final columns = state.columns;
final proxyCardType = state.proxyCardType;
return switch (type) {
ProxiesType.tab => _buildTabGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
),
ProxiesType.expansion => _buildExpansionGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
),
};
},
);
}
}
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 200,
),
);
_scale = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
),
),
child: widget.child,
);
}
}
class ProxyCard extends StatelessWidget {
final String groupName;
final Proxy proxy;
final bool isSelected;
final CommonCardType style;
final ProxyCardType type;
const ProxyCard({
super.key,
required this.groupName,
required this.proxy,
required this.isSelected,
this.style = CommonCardType.plain,
required this.type,
});
Measure get measure => globalState.appController.measure;
Widget _buildDelayText() {
return SizedBox(
height: measure.labelSmallHeight,
child: Selector<AppState, int?>(
selector: (context, appState) => appState.getDelay(
proxy.name,
),
builder: (context, delay, __) {
return FadeBox(
child: Builder(
builder: (_) {
if (delay == null) {
return Container();
}
if (delay == 0) {
return SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: const CircularProgressIndicator(
strokeWidth: 2,
),
);
}
return Text(
delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: other.getDelayColor(
delay,
),
),
);
},
),
);
},
),
);
}
Widget _buildProxyNameText(BuildContext context) {
return SizedBox(
height: measure.bodyMediumHeight * 2,
child: Text(
proxy.name,
maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
),
);
}
_changeProxy(BuildContext context) {
final appController = globalState.appController;
final group = appController.appState.getGroupWithName(groupName)!;
if (group.type != GroupType.Selector) {
globalState.showSnackBar(
context,
message: appLocalizations.notSelectedTip,
);
return;
}
globalState.appController.config.updateCurrentSelectedMap(
groupName,
proxy.name,
);
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxy.name,
),
);
}
@override
Widget build(BuildContext context) {
final measure = globalState.appController.measure;
final delayText = _buildDelayText();
final proxyNameText = _buildProxyNameText(context);
return CommonCard(
type: style,
key: key,
onPressed: () {
_changeProxy(context);
},
isSelected: isSelected,
child: Container(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
proxyNameText,
const SizedBox(
height: 8,
),
if (type == ProxyCardType.expand) ...[
SizedBox(
height: measure.bodySmallHeight,
child: Selector<AppState, String>(
selector: (context, appState) => appState.getDesc(
proxy.type,
proxy.name,
),
builder: (_, desc, __) {
return TooltipText(
text: Text(
desc,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color?.toLight(),
),
),
);
},
),
),
const SizedBox(
height: 8,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color:
context.textTheme.bodySmall?.color?.toLight(),
),
),
),
),
delayText,
],
),
)
],
),
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/card.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProxiesExpansionPanelFragment extends StatefulWidget {
const ProxiesExpansionPanelFragment({super.key});
@override
State<ProxiesExpansionPanelFragment> createState() =>
_ProxiesExpansionPanelFragmentState();
}
class _ProxiesExpansionPanelFragmentState
extends State<ProxiesExpansionPanelFragment> {
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: state.groupNames.length,
itemBuilder: (_, index) {
final groupName = state.groupNames[index];
return CommonCard(
child: ExpansionTile(
title: Text(groupName),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0.0),
side: const BorderSide(color: Colors.transparent),
),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0.0),
side: const BorderSide(color: Colors.transparent),
),
children: [
Text("1212313"),
],
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 16,
);
},
);
},
);
}
}

View File

@@ -0,0 +1,72 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/tabview.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProxiesFragment extends StatefulWidget {
const ProxiesFragment({super.key});
@override
State<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
final items = [
CommonPopupMenuItem(
action: ProxiesSortType.none,
label: appLocalizations.defaultSort,
iconData: Icons.sort,
),
CommonPopupMenuItem(
action: ProxiesSortType.delay,
label: appLocalizations.delaySort,
iconData: Icons.network_ping),
CommonPopupMenuItem(
action: ProxiesSortType.name,
label: appLocalizations.nameSort,
iconData: Icons.sort_by_alpha),
];
commonScaffoldState?.actions = [
Selector<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesSortType,
builder: (_, proxiesSortType, __) {
return CommonPopupMenu<ProxiesSortType>.radio(
items: items,
onSelected: (value) {
final config = context.read<Config>();
config.proxiesSortType = value;
},
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: const ProxiesTabFragment(),
);
}
}

View File

@@ -0,0 +1,470 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProxiesTabFragment extends StatefulWidget {
const ProxiesTabFragment({super.key});
@override
State<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
}
class _ProxiesTabFragmentState extends State<ProxiesTabFragment> with TickerProviderStateMixin {
TabController? _tabController;
_handleTabControllerChange() {
final indexIsChanging = _tabController?.indexIsChanging ?? false;
if (indexIsChanging) return;
final index = _tabController?.index;
if (index == null) return;
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(currentGroups[index].name);
}
}
@override
void dispose() {
super.dispose();
_tabController?.dispose();
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_tabController?.removeListener(_handleTabControllerChange);
_tabController?.dispose();
_tabController = null;
return true;
}
return false;
},
builder: (_, state, __) {
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
)..addListener(_handleTabControllerChange);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: _tabController,
padding: const EdgeInsets.symmetric(horizontal: 16),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxiesTabView(
groupName: groupName,
),
),
],
),
)
],
);
},
);
}
}
class ProxiesTabView extends StatelessWidget {
final String groupName;
const ProxiesTabView({
super.key,
required this.groupName,
});
List<Proxy> _sortOfName(List<Proxy> proxies) {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(a.name, b.name),
);
}
List<Proxy> _sortOfDelay(BuildContext context, List<Proxy> proxies) {
final appState = context.read<AppState>();
return proxies = List.of(proxies)
..sort(
(a, b) {
final aDelay = appState.getDelay(a.name);
final bDelay = appState.getDelay(b.name);
if (aDelay == null && bDelay == null) {
return 0;
}
if (aDelay == null || aDelay == -1) {
return 1;
}
if (bDelay == null || bDelay == -1) {
return -1;
}
return aDelay.compareTo(bDelay);
},
);
}
_getProxies(
BuildContext context,
List<Proxy> proxies,
ProxiesSortType proxiesSortType,
) {
if (proxiesSortType == ProxiesSortType.delay) {
return _sortOfDelay(context, proxies);
}
if (proxiesSortType == ProxiesSortType.name) return _sortOfName(proxies);
return proxies;
}
double _getItemHeight(BuildContext context) {
final measure = globalState.appController.measure;
return 12 * 2 +
measure.bodyMediumHeight * 2 +
measure.bodySmallHeight +
measure.labelSmallHeight +
8 * 2;
}
int _getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
return 2;
case ViewMode.laptop:
return 3;
case ViewMode.desktop:
return 4;
}
}
_delayTest(List<Proxy> proxies) async {
for (final proxy in proxies) {
final appController = globalState.appController;
final proxyName =
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(
name: proxyName,
value: 0,
),
);
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesTabViewSelectorState>(
selector: (_, appState, config) {
return ProxiesTabViewSelectorState(
proxiesSortType: config.proxiesSortType,
sortNum: appState.sortNum,
group: appState.getGroupWithName(groupName)!,
viewMode: appState.viewMode,
);
},
builder: (_, state, __) {
final proxies = _getProxies(
context,
state.group.all,
state.proxiesSortType,
);
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
state.group.all,
);
},
child: Align(
alignment: Alignment.topCenter,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _getColumns(state.viewMode),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(context),
),
itemCount: proxies.length,
itemBuilder: (_, index) {
final proxy = proxies[index];
return ProxyCard(
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
),
),
);
},
);
}
}
class ProxyCard extends StatelessWidget {
final String groupName;
final Proxy proxy;
const ProxyCard({
super.key,
required this.groupName,
required this.proxy,
});
@override
Widget build(BuildContext context) {
final measure = globalState.appController.measure;
return Selector3<AppState, Config, ClashConfig, ProxiesCardSelectorState>(
selector: (_, appState, config, clashConfig) {
final group = appState.getGroupWithName(groupName)!;
bool isSelected = config.currentSelectedMap[group.name] == proxy.name ||
(config.currentSelectedMap[group.name] == null &&
group.now == proxy.name);
return ProxiesCardSelectorState(
isSelected: isSelected,
);
},
builder: (_, state, __) {
return CommonCard(
isSelected: state.isSelected,
onPressed: () {
final appController = globalState.appController;
final group = appController.appState.getGroupWithName(groupName)!;
if (group.type != GroupType.Selector) {
globalState.showSnackBar(
context,
message: appLocalizations.notSelectedTip,
);
return;
}
globalState.appController.config.updateCurrentSelectedMap(
groupName,
proxy.name,
);
globalState.appController.changeProxy();
},
selectWidget: Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: measure.bodyMediumHeight * 2,
child: Text(
proxy.name,
maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(
height: 8,
),
SizedBox(
height: measure.bodySmallHeight,
child: Selector<AppState, String>(
selector: (context, appState) => appState.getDesc(
proxy.type,
proxy.name,
),
builder: (_, desc, __) {
return TooltipText(
text: Text(
desc,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color:
context.textTheme.bodySmall?.color?.toLight(),
),
),
);
},
),
),
const SizedBox(
height: 8,
),
SizedBox(
height: measure.labelSmallHeight,
child: Selector<AppState, int?>(
selector: (context, appState) => appState.getDelay(
proxy.name,
),
builder: (_, delay, __) {
return FadeBox(
child: Builder(
builder: (_) {
if (delay == null) {
return Container();
}
if (delay == 0) {
return SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: const CircularProgressIndicator(
strokeWidth: 2,
),
);
}
return Text(
delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: other.getDelayColor(
delay,
),
),
);
},
),
);
},
),
),
],
),
),
);
},
);
}
}
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 200,
),
);
_scale = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
),
),
child: widget.child,
);
}
}

View File

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

View File

@@ -2,37 +2,22 @@ import 'dart:io';
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/state.dart';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' hide context;
import 'package:provider/provider.dart';
@immutable
class GeoItem {
final String label;
final String key;
final String fileName;
const GeoItem({
required this.label,
required this.key,
required this.fileName,
});
}
@immutable
class FileInfo {
final String size;
final DateTime lastModified;
const FileInfo({
required this.size,
required this.lastModified,
});
}
class Resources extends StatefulWidget {
const Resources({super.key});
@@ -41,473 +26,147 @@ class Resources extends StatefulWidget {
}
class _ResourcesState extends State<Resources> {
List<ExternalProvider> externalProviders = [];
List<GlobalObjectKey<_ProviderItemState>> providerItemKeys = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncExternalProviders();
_updateExternalProvider(
String providerName,
String providerType,
) async {
final commonScaffoldState = context.commonScaffoldState;
await commonScaffoldState?.loadingRun(() async {
final message = await clashCore.updateExternalProvider(
providerName: providerName,
providerType: providerType,
);
if (message.isNotEmpty) throw message;
});
}
_syncExternalProviders() async {
externalProviders = await clashCore.getExternalProviders();
if (mounted) {
setState(() {});
}
}
_updateProviders() async {
final updateProviders = providerItemKeys.map<Future>(
(key) async => await key.currentState?.updateProvider(false),
);
await Future.wait(updateProviders);
_syncExternalProviders();
}
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>[
GeoItem(
label: "GeoIp",
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 generateInfoSection(
info: Info(
iconData: Icons.storage,
label: appLocalizations.geoData,
),
items: geoItems.map(
(geoItem) => GeoDataListItem(
geoItem: geoItem,
),
),
);
}
@override
Widget build(BuildContext context) {
return generateListView(
[
..._buildGeoDataSection(),
..._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;
Future<DateTime> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
return await File(join(homePath, fileName)).lastModified();
}
@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,
Widget _buildExternalProviderSection() {
return FutureBuilder<List<ExternalProvider>>(
future: () async {
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(),
],
),
)
],
),
),
),
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(),
Widget _buildGeoDataSection() {
const geoItems = <GeoItem>[
GeoItem(label: "GeoIp", fileName: mmdbFileName),
GeoItem(label: "GeoSite", fileName: geoSiteFileName),
GeoItem(label: "ASN", fileName: asnFileName),
];
return Section(
title: appLocalizations.geoData,
child: Column(
children: [
for (final geoItem in geoItems)
ListItem(
title: Text(geoItem.label),
subtitle: FutureBuilder<DateTime>(
future: () async {
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,
);
},
),
),
],
),
],
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
);
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
_buildGeoDataSection(),
_buildExternalProviderSection(),
],
);
}
}
}

View File

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

View File

@@ -37,7 +37,7 @@
"overrideDesc": "Override Proxy related config",
"allowLan": "AllowLan",
"allowLanDesc": "Allow access proxy through the LAN",
"tun": "TUN mode",
"tun": "Tun mode",
"tunDesc": "only effective in administrator mode",
"minimizeOnExit": "Minimize on exit",
"minimizeOnExitDesc": "Modify the default system exit event",
@@ -111,7 +111,7 @@
"noMoreInfoDesc": "No more info",
"profileParseErrorDesc": "profile parse error",
"proxyPort": "ProxyPort",
"proxyPortDesc": "Set the Clash listening port",
"proxyPortDesc": "Set the clash listening port",
"port": "Port",
"logLevel": "LogLevel",
"show": "Show",
@@ -161,19 +161,20 @@
"checking": "Checking...",
"country": "Country",
"checkError": "Check error",
"ipCheckTimeout": "Ip check timeout",
"search": "Search",
"allowBypass": "Allow applications to bypass VPN",
"allowBypassDesc": "Some apps can bypass VPN when turned on",
"externalController": "ExternalController",
"externalControllerDesc": "Once enabled, the Clash kernel can be controlled on port 9090",
"ipv6Desc": "When turned on it will be able to receive IPv6 traffic",
"externalControllerDesc": "Once enabled, the clash kernel can be controlled on port 9090",
"ipv6Desc": "When turned on it will be able to receive ipv6 traffic",
"app": "App",
"general": "General",
"systemProxyDesc": "Attach HTTP proxy to VpnService",
"unifiedDelay": "Unified delay",
"unifiedDelayDesc": "Remove extra delays such as handshaking",
"tcpConcurrent": "TCP concurrent",
"tcpConcurrentDesc": "Enabling it will allow TCP concurrency",
"tcpConcurrent": "Tcp concurrent",
"tcpConcurrentDesc": "Enabling it will allow tcp concurrency",
"geodataLoader": "Geo Low Memory Mode",
"geodataLoaderDesc": "Enabling will use the Geo low memory loader",
"requests": "Requests",
@@ -187,13 +188,9 @@
"connectionsDesc": "View current connection",
"nullRequestsDesc": "No requests",
"nullConnectionsDesc": "No connections",
"intranetIP": "Intranet IP",
"intranetIp": "Intranet IP",
"view": "View",
"cut": "Cut",
"copy": "Copy",
"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"
"paste": "Paste"
}

View File

@@ -37,7 +37,7 @@
"overrideDesc": "覆写代理相关配置",
"allowLan": "局域网代理",
"allowLanDesc": "允许通过局域网访问代理",
"tun": "TUN模式",
"tun": "Tun模式",
"tunDesc": "仅在管理员模式生效",
"minimizeOnExit": "退出时最小化",
"minimizeOnExitDesc": "修改系统默认退出事件",
@@ -111,7 +111,7 @@
"noMoreInfoDesc": "暂无更多信息",
"profileParseErrorDesc": "配置文件解析错误",
"proxyPort": "代理端口",
"proxyPortDesc": "设置Clash监听端口",
"proxyPortDesc": "设置clash监听端口",
"port": "端口",
"logLevel": "日志等级",
"show": "显示",
@@ -161,19 +161,20 @@
"checking": "检测中...",
"country": "区域",
"checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时",
"search": "搜索",
"allowBypass": "允许应用绕过VPN",
"allowBypass": "允许应用绕过vpn",
"allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
"ipv6Desc": "开启后将可以接收IPv6流量",
"externalControllerDesc": "开启后将可以通过9090端口控制clash内核",
"ipv6Desc": "开启后将可以接收ipv6流量",
"app": "应用",
"general": "基础",
"systemProxyDesc": "为VpnService附加HTTP代理",
"unifiedDelay": "统一延迟",
"unifiedDelayDesc": "去除握手等额外延迟",
"tcpConcurrent": "TCP并发",
"tcpConcurrentDesc": "开启后允许TCP并发",
"tcpConcurrentDesc": "开启后允许tcp并发",
"geodataLoader": "Geo低内存模式",
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求",
@@ -187,13 +188,9 @@
"connectionsDesc": "查看当前连接",
"nullRequestsDesc": "暂无请求",
"nullConnectionsDesc": "暂无连接",
"intranetIP": "内网 IP",
"intranetIp": "内网 IP",
"view": "查看",
"cut": "剪切",
"copy": "复制",
"paste": "粘贴",
"testUrl": "测速链接",
"sync": "同步",
"exclude": "从最近任务中隐藏",
"excludeDesc": "应用在后台时,从最近任务中隐藏应用"
"paste": "粘贴"
}

View File

@@ -121,17 +121,13 @@ class MessageLookup extends MessageLookupByLibrary {
"download": MessageLookupByLibrary.simpleMessage("Download"),
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"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"),
"expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Once enabled, the Clash kernel can be controlled on port 9090"),
"Once enabled, the clash kernel can be controlled on port 9090"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
@@ -156,9 +152,11 @@ class MessageLookup extends MessageLookupByLibrary {
"infiniteTime":
MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"),
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"intranetIp": MessageLookupByLibrary.simpleMessage("Intranet IP"),
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive IPv6 traffic"),
"When turned on it will be able to receive ipv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
@@ -232,7 +230,7 @@ class MessageLookup extends MessageLookupByLibrary {
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the Clash listening port"),
"Set the clash listening port"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Scan QR code to obtain profile"),
@@ -264,17 +262,15 @@ class MessageLookup extends MessageLookupByLibrary {
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"When enabled, the home tab will add a toggle animation"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP concurrent"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("Tcp concurrent"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage(
"Enabling it will allow TCP concurrency"),
"testUrl": MessageLookupByLibrary.simpleMessage("Test url"),
"Enabling it will allow tcp concurrency"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage(
@@ -283,7 +279,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tun": MessageLookupByLibrary.simpleMessage("Tun mode"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"),
"unableToUpdateCurrentProfileDesc":

View File

@@ -36,7 +36,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"),
"allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
@@ -100,14 +100,11 @@ class MessageLookup extends MessageLookupByLibrary {
"download": MessageLookupByLibrary.simpleMessage("下载"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制clash内核"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
@@ -126,8 +123,9 @@ class MessageLookup extends MessageLookupByLibrary {
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"intranetIp": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
@@ -188,7 +186,7 @@ class MessageLookup extends MessageLookupByLibrary {
"project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置clash监听端口"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
@@ -212,7 +210,6 @@ class MessageLookup extends MessageLookupByLibrary {
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
@@ -220,8 +217,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tabAnimationDesc":
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许tcp并发"),
"theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
@@ -229,7 +225,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
"tun": MessageLookupByLibrary.simpleMessage("Tun模式"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),

View File

@@ -430,10 +430,10 @@ class AppLocalizations {
);
}
/// `TUN mode`
/// `Tun mode`
String get tun {
return Intl.message(
'TUN mode',
'Tun mode',
name: 'tun',
desc: '',
args: [],
@@ -1170,10 +1170,10 @@ class AppLocalizations {
);
}
/// `Set the Clash listening port`
/// `Set the clash listening port`
String get proxyPortDesc {
return Intl.message(
'Set the Clash listening port',
'Set the clash listening port',
name: 'proxyPortDesc',
desc: '',
args: [],
@@ -1670,6 +1670,16 @@ class AppLocalizations {
);
}
/// `Ip check timeout`
String get ipCheckTimeout {
return Intl.message(
'Ip check timeout',
name: 'ipCheckTimeout',
desc: '',
args: [],
);
}
/// `Search`
String get search {
return Intl.message(
@@ -1710,20 +1720,20 @@ class AppLocalizations {
);
}
/// `Once enabled, the Clash kernel can be controlled on port 9090`
/// `Once enabled, the clash kernel can be controlled on port 9090`
String get externalControllerDesc {
return Intl.message(
'Once enabled, the Clash kernel can be controlled on port 9090',
'Once enabled, the clash kernel can be controlled on port 9090',
name: 'externalControllerDesc',
desc: '',
args: [],
);
}
/// `When turned on it will be able to receive IPv6 traffic`
/// `When turned on it will be able to receive ipv6 traffic`
String get ipv6Desc {
return Intl.message(
'When turned on it will be able to receive IPv6 traffic',
'When turned on it will be able to receive ipv6 traffic',
name: 'ipv6Desc',
desc: '',
args: [],
@@ -1780,20 +1790,20 @@ class AppLocalizations {
);
}
/// `TCP concurrent`
/// `Tcp concurrent`
String get tcpConcurrent {
return Intl.message(
'TCP concurrent',
'Tcp concurrent',
name: 'tcpConcurrent',
desc: '',
args: [],
);
}
/// `Enabling it will allow TCP concurrency`
/// `Enabling it will allow tcp concurrency`
String get tcpConcurrentDesc {
return Intl.message(
'Enabling it will allow TCP concurrency',
'Enabling it will allow tcp concurrency',
name: 'tcpConcurrentDesc',
desc: '',
args: [],
@@ -1931,10 +1941,10 @@ class AppLocalizations {
}
/// `Intranet IP`
String get intranetIP {
String get intranetIp {
return Intl.message(
'Intranet IP',
name: 'intranetIP',
name: 'intranetIp',
desc: '',
args: [],
);
@@ -1979,46 +1989,6 @@ class AppLocalizations {
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> {

View File

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

View File

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

View File

@@ -1,10 +1,7 @@
// 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/state.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -107,8 +104,6 @@ class Dns {
}
}
typedef GeoXMap = Map<String, String>;
@JsonSerializable()
class ClashConfig extends ChangeNotifier {
int _mixedPort;
@@ -123,9 +118,7 @@ class ClashConfig extends ChangeNotifier {
bool _tcpConcurrent;
Tun _tun;
Dns _dns;
GeoXMap _geoXUrl;
List<String> _rules;
String? _globalRealUa;
ClashConfig()
: _mixedPort = 7890,
@@ -140,7 +133,6 @@ class ClashConfig extends ChangeNotifier {
_geodataLoader = geodataLoaderMemconservative,
_externalController = '',
_dns = Dns(),
_geoXUrl = defaultGeoXMap,
_rules = [];
@JsonKey(name: "mixed-port", defaultValue: 7890)
@@ -243,12 +235,7 @@ class ClashConfig extends ChangeNotifier {
}
}
Tun get tun {
if (Platform.isAndroid) {
return _tun.copyWith(enable: false);
}
return _tun;
}
Tun get tun => _tun;
set tun(Tun value) {
if (_tun != value) {
@@ -275,35 +262,6 @@ 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]) {
if (clashConfig != null) {
_mixedPort = clashConfig._mixedPort;
@@ -313,7 +271,6 @@ class ClashConfig extends ChangeNotifier {
_tun = clashConfig._tun;
_dns = clashConfig._dns;
_rules = clashConfig._rules;
_globalRealUa = clashConfig.globalRealUa;
}
notifyListeners();
}
@@ -325,4 +282,9 @@ class ClashConfig extends ChangeNotifier {
factory ClashConfig.fromJson(Map<String, dynamic> 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

@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -56,12 +55,7 @@ class Config extends ChangeNotifier {
bool _autoCheckUpdate;
bool _allowBypass;
bool _systemProxy;
bool _isExclude;
DAV? _dav;
ProxiesType _proxiesType;
ProxyCardType _proxyCardType;
int _proxiesColumns;
String _testUrl;
Config()
: _profiles = [],
@@ -77,14 +71,9 @@ class Config extends ChangeNotifier {
_isAccessControl = false,
_autoCheckUpdate = true,
_systemProxy = true,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
_allowBypass = true,
_isExclude = false,
_proxyCardType = ProxyCardType.expand,
_proxiesType = ProxiesType.tab,
_proxiesColumns = 2;
_allowBypass = true;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -161,19 +150,6 @@ class Config extends ChangeNotifier {
String? get currentGroupName => currentProfile?.currentGroupName;
Set<String> get currentUnfoldSet => currentProfile?.unfoldSet ?? {};
updateCurrentUnfoldSet(Set<String> value) {
if (!const SetEquality<String>().equals(currentUnfoldSet, value)) {
_setProfile(
currentProfile!.copyWith(
unfoldSet: value,
),
);
notifyListeners();
}
}
updateCurrentGroupName(String groupName) {
if (currentProfile != null &&
currentProfile!.currentGroupName != groupName) {
@@ -388,56 +364,6 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: ProxiesType.tab)
ProxiesType get proxiesType => _proxiesType;
set proxiesType(ProxiesType value) {
if (_proxiesType != value) {
_proxiesType = value;
notifyListeners();
}
}
@JsonKey(defaultValue: ProxyCardType.expand)
ProxyCardType get proxyCardType => _proxyCardType;
set proxyCardType(ProxyCardType value) {
if (_proxyCardType != value) {
_proxyCardType = value;
notifyListeners();
}
}
@JsonKey(defaultValue: 2)
int get proxiesColumns => _proxiesColumns;
set proxiesColumns(int value) {
if (_proxiesColumns != value) {
_proxiesColumns = value;
notifyListeners();
}
}
@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([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -457,7 +383,6 @@ class Config extends ChangeNotifier {
_autoLaunch = config._autoLaunch;
_silentLaunch = config._silentLaunch;
_autoRun = config._autoRun;
_proxiesType = config._proxiesType;
_openLog = config._openLog;
_themeMode = config._themeMode;
_locale = config._locale;
@@ -470,7 +395,6 @@ class Config extends ChangeNotifier {
_isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate;
_dav = config._dav;
_testUrl = config.testUrl;
}
notifyListeners();
}

View File

@@ -3,32 +3,19 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/connection.dart';
import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/ffi.g.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
class UpdateConfigParams with _$UpdateConfigParams {
const factory UpdateConfigParams({
@JsonKey(name: "profile-path") String? profilePath,
required ClashConfig config,
required ConfigExtendedParams params,
@JsonKey(name: "is-patch") required bool isPatch,
@JsonKey(name: "is-compatible") required bool isCompatible,
}) = _UpdateConfigParams;
factory UpdateConfigParams.fromJson(Map<String, Object?> json) =>
@@ -47,25 +34,14 @@ class ChangeProxyParams with _$ChangeProxyParams {
}
@freezed
class AppMessage with _$AppMessage {
const factory AppMessage({
required AppMessageType type,
class Message with _$Message {
const factory Message({
required MessageType type,
dynamic data,
}) = _AppMessage;
}) = _Message;
factory AppMessage.fromJson(Map<String, Object?> 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);
factory Message.fromJson(Map<String, Object?> json) =>
_$MessageFromJson(json);
}
@freezed
@@ -99,16 +75,6 @@ class Process with _$Process {
_$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
class ProcessMapItem with _$ProcessMapItem {
const factory ProcessMapItem({
@@ -132,27 +98,3 @@ class ExternalProvider with _$ExternalProvider {
factory ExternalProvider.fromJson(Map<String, Object?> 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,21 +51,7 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false
..tun = Tun.fromJson(json['tun'] 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()
..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'
};
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList();
Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
<String, dynamic>{
@@ -82,8 +68,6 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,
'global-real-ua': instance.globalRealUa,
'geox-url': instance.geoXUrl,
};
const _$ModeEnumMap = {

View File

@@ -34,17 +34,7 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true
..proxiesType =
$enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType']) ??
ProxiesType.tab
..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand
..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;
..systemProxy = json['systemProxy'] as bool? ?? true;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -66,11 +56,6 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl,
'isExclude': instance.isExclude,
};
const _$ThemeModeEnumMap = {
@@ -85,16 +70,6 @@ const _$ProxiesSortTypeEnumMap = {
ProxiesSortType.name: 'name',
};
const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab',
ProxiesType.expansion: 'expansion',
};
const _$ProxyCardTypeEnumMap = {
ProxyCardType.expand: 'expand',
ProxyCardType.shrink: 'shrink',
};
_$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
_$AccessControlImpl(
mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ??

View File

@@ -14,234 +14,6 @@ T _$identity<T>(T value) => value;
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');
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) {
return _UpdateConfigParams.fromJson(json);
}
@@ -251,7 +23,10 @@ mixin _$UpdateConfigParams {
@JsonKey(name: "profile-path")
String? get profilePath => throw _privateConstructorUsedError;
ClashConfig get config => throw _privateConstructorUsedError;
ConfigExtendedParams get params => throw _privateConstructorUsedError;
@JsonKey(name: "is-patch")
bool get isPatch => throw _privateConstructorUsedError;
@JsonKey(name: "is-compatible")
bool get isCompatible => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -268,9 +43,8 @@ abstract class $UpdateConfigParamsCopyWith<$Res> {
$Res call(
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
ConfigExtendedParams params});
$ConfigExtendedParamsCopyWith<$Res> get params;
@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible});
}
/// @nodoc
@@ -288,7 +62,8 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
$Res call({
Object? profilePath = freezed,
Object? config = null,
Object? params = null,
Object? isPatch = null,
Object? isCompatible = null,
}) {
return _then(_value.copyWith(
profilePath: freezed == profilePath
@@ -299,20 +74,16 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams>
? _value.config
: config // ignore: cast_nullable_to_non_nullable
as ClashConfig,
params: null == params
? _value.params
: params // ignore: cast_nullable_to_non_nullable
as ConfigExtendedParams,
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,
) 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
@@ -326,10 +97,8 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res>
$Res call(
{@JsonKey(name: "profile-path") String? profilePath,
ClashConfig config,
ConfigExtendedParams params});
@override
$ConfigExtendedParamsCopyWith<$Res> get params;
@JsonKey(name: "is-patch") bool isPatch,
@JsonKey(name: "is-compatible") bool isCompatible});
}
/// @nodoc
@@ -345,7 +114,8 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
$Res call({
Object? profilePath = freezed,
Object? config = null,
Object? params = null,
Object? isPatch = null,
Object? isCompatible = null,
}) {
return _then(_$UpdateConfigParamsImpl(
profilePath: freezed == profilePath
@@ -356,10 +126,14 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res>
? _value.config
: config // ignore: cast_nullable_to_non_nullable
as ClashConfig,
params: null == params
? _value.params
: params // ignore: cast_nullable_to_non_nullable
as ConfigExtendedParams,
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,
));
}
}
@@ -370,7 +144,8 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
const _$UpdateConfigParamsImpl(
{@JsonKey(name: "profile-path") this.profilePath,
required this.config,
required this.params});
@JsonKey(name: "is-patch") required this.isPatch,
@JsonKey(name: "is-compatible") required this.isCompatible});
factory _$UpdateConfigParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$UpdateConfigParamsImplFromJson(json);
@@ -381,11 +156,15 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
@override
final ClashConfig config;
@override
final ConfigExtendedParams params;
@JsonKey(name: "is-patch")
final bool isPatch;
@override
@JsonKey(name: "is-compatible")
final bool isCompatible;
@override
String toString() {
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)';
return 'UpdateConfigParams(profilePath: $profilePath, config: $config, isPatch: $isPatch, isCompatible: $isCompatible)';
}
@override
@@ -396,12 +175,15 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
(identical(other.profilePath, profilePath) ||
other.profilePath == profilePath) &&
(identical(other.config, config) || other.config == config) &&
(identical(other.params, params) || other.params == params));
(identical(other.isPatch, isPatch) || other.isPatch == isPatch) &&
(identical(other.isCompatible, isCompatible) ||
other.isCompatible == isCompatible));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, profilePath, config, params);
int get hashCode =>
Object.hash(runtimeType, profilePath, config, isPatch, isCompatible);
@JsonKey(ignore: true)
@override
@@ -420,9 +202,11 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams {
abstract class _UpdateConfigParams implements UpdateConfigParams {
const factory _UpdateConfigParams(
{@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config,
required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl;
{@JsonKey(name: "profile-path") final String? profilePath,
required final ClashConfig config,
@JsonKey(name: "is-patch") required final bool isPatch,
@JsonKey(name: "is-compatible") required final bool isCompatible}) =
_$UpdateConfigParamsImpl;
factory _UpdateConfigParams.fromJson(Map<String, dynamic> json) =
_$UpdateConfigParamsImpl.fromJson;
@@ -433,7 +217,11 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
@override
ClashConfig get config;
@override
ConfigExtendedParams get params;
@JsonKey(name: "is-patch")
bool get isPatch;
@override
@JsonKey(name: "is-compatible")
bool get isCompatible;
@override
@JsonKey(ignore: true)
_$$UpdateConfigParamsImplCopyWith<_$UpdateConfigParamsImpl> get copyWith =>
@@ -610,34 +398,32 @@ abstract class _ChangeProxyParams implements ChangeProxyParams {
throw _privateConstructorUsedError;
}
AppMessage _$AppMessageFromJson(Map<String, dynamic> json) {
return _AppMessage.fromJson(json);
Message _$MessageFromJson(Map<String, dynamic> json) {
return _Message.fromJson(json);
}
/// @nodoc
mixin _$AppMessage {
AppMessageType get type => throw _privateConstructorUsedError;
mixin _$Message {
MessageType get type => throw _privateConstructorUsedError;
dynamic get data => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AppMessageCopyWith<AppMessage> get copyWith =>
throw _privateConstructorUsedError;
$MessageCopyWith<Message> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AppMessageCopyWith<$Res> {
factory $AppMessageCopyWith(
AppMessage value, $Res Function(AppMessage) then) =
_$AppMessageCopyWithImpl<$Res, AppMessage>;
abstract class $MessageCopyWith<$Res> {
factory $MessageCopyWith(Message value, $Res Function(Message) then) =
_$MessageCopyWithImpl<$Res, Message>;
@useResult
$Res call({AppMessageType type, dynamic data});
$Res call({MessageType type, dynamic data});
}
/// @nodoc
class _$AppMessageCopyWithImpl<$Res, $Val extends AppMessage>
implements $AppMessageCopyWith<$Res> {
_$AppMessageCopyWithImpl(this._value, this._then);
class _$MessageCopyWithImpl<$Res, $Val extends Message>
implements $MessageCopyWith<$Res> {
_$MessageCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
@@ -654,7 +440,7 @@ class _$AppMessageCopyWithImpl<$Res, $Val extends AppMessage>
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as AppMessageType,
as MessageType,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
@@ -664,22 +450,21 @@ class _$AppMessageCopyWithImpl<$Res, $Val extends AppMessage>
}
/// @nodoc
abstract class _$$AppMessageImplCopyWith<$Res>
implements $AppMessageCopyWith<$Res> {
factory _$$AppMessageImplCopyWith(
_$AppMessageImpl value, $Res Function(_$AppMessageImpl) then) =
__$$AppMessageImplCopyWithImpl<$Res>;
abstract class _$$MessageImplCopyWith<$Res> implements $MessageCopyWith<$Res> {
factory _$$MessageImplCopyWith(
_$MessageImpl value, $Res Function(_$MessageImpl) then) =
__$$MessageImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({AppMessageType type, dynamic data});
$Res call({MessageType type, dynamic data});
}
/// @nodoc
class __$$AppMessageImplCopyWithImpl<$Res>
extends _$AppMessageCopyWithImpl<$Res, _$AppMessageImpl>
implements _$$AppMessageImplCopyWith<$Res> {
__$$AppMessageImplCopyWithImpl(
_$AppMessageImpl _value, $Res Function(_$AppMessageImpl) _then)
class __$$MessageImplCopyWithImpl<$Res>
extends _$MessageCopyWithImpl<$Res, _$MessageImpl>
implements _$$MessageImplCopyWith<$Res> {
__$$MessageImplCopyWithImpl(
_$MessageImpl _value, $Res Function(_$MessageImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@@ -688,11 +473,11 @@ class __$$AppMessageImplCopyWithImpl<$Res>
Object? type = null,
Object? data = freezed,
}) {
return _then(_$AppMessageImpl(
return _then(_$MessageImpl(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as AppMessageType,
as MessageType,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
@@ -703,27 +488,27 @@ class __$$AppMessageImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$AppMessageImpl implements _AppMessage {
const _$AppMessageImpl({required this.type, this.data});
class _$MessageImpl implements _Message {
const _$MessageImpl({required this.type, this.data});
factory _$AppMessageImpl.fromJson(Map<String, dynamic> json) =>
_$$AppMessageImplFromJson(json);
factory _$MessageImpl.fromJson(Map<String, dynamic> json) =>
_$$MessageImplFromJson(json);
@override
final AppMessageType type;
final MessageType type;
@override
final dynamic data;
@override
String toString() {
return 'AppMessage(type: $type, data: $data)';
return 'Message(type: $type, data: $data)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AppMessageImpl &&
other is _$MessageImpl &&
(identical(other.type, type) || other.type == type) &&
const DeepCollectionEquality().equals(other.data, data));
}
@@ -736,188 +521,30 @@ class _$AppMessageImpl implements _AppMessage {
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AppMessageImplCopyWith<_$AppMessageImpl> get copyWith =>
__$$AppMessageImplCopyWithImpl<_$AppMessageImpl>(this, _$identity);
_$$MessageImplCopyWith<_$MessageImpl> get copyWith =>
__$$MessageImplCopyWithImpl<_$MessageImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AppMessageImplToJson(
return _$$MessageImplToJson(
this,
);
}
}
abstract class _AppMessage implements AppMessage {
const factory _AppMessage(
{required final AppMessageType type,
final dynamic data}) = _$AppMessageImpl;
abstract class _Message implements Message {
const factory _Message(
{required final MessageType type, final dynamic data}) = _$MessageImpl;
factory _AppMessage.fromJson(Map<String, dynamic> json) =
_$AppMessageImpl.fromJson;
factory _Message.fromJson(Map<String, dynamic> json) = _$MessageImpl.fromJson;
@override
AppMessageType get type;
MessageType get type;
@override
dynamic get data;
@override
@JsonKey(ignore: true)
_$$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 =>
_$$MessageImplCopyWith<_$MessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -1379,151 +1006,6 @@ abstract class _Process implements Process {
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) {
return _ProcessMapItem.fromJson(json);
}

View File

@@ -6,31 +6,13 @@ part of '../ffi.dart';
// 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(
Map<String, dynamic> json) =>
_$UpdateConfigParamsImpl(
profilePath: json['profile-path'] as String?,
config: ClashConfig.fromJson(json['config'] as Map<String, dynamic>),
params:
ConfigExtendedParams.fromJson(json['params'] as Map<String, dynamic>),
isPatch: json['is-patch'] as bool,
isCompatible: json['is-compatible'] as bool,
);
Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
@@ -38,7 +20,8 @@ Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
<String, dynamic>{
'profile-path': instance.profilePath,
'config': instance.config,
'params': instance.params,
'is-patch': instance.isPatch,
'is-compatible': instance.isCompatible,
};
_$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson(
@@ -55,44 +38,26 @@ Map<String, dynamic> _$$ChangeProxyParamsImplToJson(
'proxy-name': instance.proxyName,
};
_$AppMessageImpl _$$AppMessageImplFromJson(Map<String, dynamic> json) =>
_$AppMessageImpl(
type: $enumDecode(_$AppMessageTypeEnumMap, json['type']),
_$MessageImpl _$$MessageImplFromJson(Map<String, dynamic> json) =>
_$MessageImpl(
type: $enumDecode(_$MessageTypeEnumMap, json['type']),
data: json['data'],
);
Map<String, dynamic> _$$AppMessageImplToJson(_$AppMessageImpl instance) =>
Map<String, dynamic> _$$MessageImplToJson(_$MessageImpl instance) =>
<String, dynamic>{
'type': _$AppMessageTypeEnumMap[instance.type]!,
'type': _$MessageTypeEnumMap[instance.type]!,
'data': instance.data,
};
const _$AppMessageTypeEnumMap = {
AppMessageType.log: 'log',
AppMessageType.delay: 'delay',
AppMessageType.request: 'request',
AppMessageType.started: 'started',
AppMessageType.loaded: 'loaded',
};
_$ServiceMessageImpl _$$ServiceMessageImplFromJson(Map<String, dynamic> json) =>
_$ServiceMessageImpl(
type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']),
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',
const _$MessageTypeEnumMap = {
MessageType.log: 'log',
MessageType.tun: 'tun',
MessageType.delay: 'delay',
MessageType.process: 'process',
MessageType.now: 'now',
MessageType.request: 'request',
MessageType.run: 'run',
};
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(
@@ -128,16 +93,6 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
'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(
id: (json['id'] as num).toInt(),

View File

@@ -222,7 +222,6 @@ mixin _$Profile {
UserInfo? get userInfo => throw _privateConstructorUsedError;
bool get autoUpdate => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
Set<String> get unfoldSet => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -243,8 +242,7 @@ abstract class $ProfileCopyWith<$Res> {
Duration autoUpdateDuration,
UserInfo? userInfo,
bool autoUpdate,
Map<String, String> selectedMap,
Set<String> unfoldSet});
Map<String, String> selectedMap});
$UserInfoCopyWith<$Res>? get userInfo;
}
@@ -271,7 +269,6 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile>
Object? userInfo = freezed,
Object? autoUpdate = null,
Object? selectedMap = null,
Object? unfoldSet = null,
}) {
return _then(_value.copyWith(
id: null == id
@@ -310,10 +307,6 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile>
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
unfoldSet: null == unfoldSet
? _value.unfoldSet
: unfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
) as $Val);
}
@@ -346,8 +339,7 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> {
Duration autoUpdateDuration,
UserInfo? userInfo,
bool autoUpdate,
Map<String, String> selectedMap,
Set<String> unfoldSet});
Map<String, String> selectedMap});
@override
$UserInfoCopyWith<$Res>? get userInfo;
@@ -373,7 +365,6 @@ class __$$ProfileImplCopyWithImpl<$Res>
Object? userInfo = freezed,
Object? autoUpdate = null,
Object? selectedMap = null,
Object? unfoldSet = null,
}) {
return _then(_$ProfileImpl(
id: null == id
@@ -412,10 +403,6 @@ class __$$ProfileImplCopyWithImpl<$Res>
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
unfoldSet: null == unfoldSet
? _value._unfoldSet
: unfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
));
}
}
@@ -432,10 +419,8 @@ class _$ProfileImpl implements _Profile {
required this.autoUpdateDuration,
this.userInfo,
this.autoUpdate = true,
final Map<String, String> selectedMap = const {},
final Set<String> unfoldSet = const {}})
: _selectedMap = selectedMap,
_unfoldSet = unfoldSet;
final Map<String, String> selectedMap = const {}})
: _selectedMap = selectedMap;
factory _$ProfileImpl.fromJson(Map<String, dynamic> json) =>
_$$ProfileImplFromJson(json);
@@ -467,18 +452,9 @@ class _$ProfileImpl implements _Profile {
return EqualUnmodifiableMapView(_selectedMap);
}
final Set<String> _unfoldSet;
@override
@JsonKey()
Set<String> get unfoldSet {
if (_unfoldSet is EqualUnmodifiableSetView) return _unfoldSet;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_unfoldSet);
}
@override
String toString() {
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet)';
return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap)';
}
@override
@@ -500,9 +476,7 @@ class _$ProfileImpl implements _Profile {
(identical(other.autoUpdate, autoUpdate) ||
other.autoUpdate == autoUpdate) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap) &&
const DeepCollectionEquality()
.equals(other._unfoldSet, _unfoldSet));
.equals(other._selectedMap, _selectedMap));
}
@JsonKey(ignore: true)
@@ -517,8 +491,7 @@ class _$ProfileImpl implements _Profile {
autoUpdateDuration,
userInfo,
autoUpdate,
const DeepCollectionEquality().hash(_selectedMap),
const DeepCollectionEquality().hash(_unfoldSet));
const DeepCollectionEquality().hash(_selectedMap));
@JsonKey(ignore: true)
@override
@@ -544,8 +517,7 @@ abstract class _Profile implements Profile {
required final Duration autoUpdateDuration,
final UserInfo? userInfo,
final bool autoUpdate,
final Map<String, String> selectedMap,
final Set<String> unfoldSet}) = _$ProfileImpl;
final Map<String, String> selectedMap}) = _$ProfileImpl;
factory _Profile.fromJson(Map<String, dynamic> json) = _$ProfileImpl.fromJson;
@@ -568,8 +540,6 @@ abstract class _Profile implements Profile {
@override
Map<String, String> get selectedMap;
@override
Set<String> get unfoldSet;
@override
@JsonKey(ignore: true)
_$$ProfileImplCopyWith<_$ProfileImpl> get copyWith =>
throw _privateConstructorUsedError;

View File

@@ -41,10 +41,6 @@ _$ProfileImpl _$$ProfileImplFromJson(Map<String, dynamic> json) =>
(k, e) => MapEntry(k, e as String),
) ??
const {},
unfoldSet: (json['unfoldSet'] as List<dynamic>?)
?.map((e) => e as String)
.toSet() ??
const {},
);
Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) =>
@@ -58,5 +54,4 @@ Map<String, dynamic> _$$ProfileImplToJson(_$ProfileImpl instance) =>
'userInfo': instance.userInfo,
'autoUpdate': instance.autoUpdate,
'selectedMap': instance.selectedMap,
'unfoldSet': instance.unfoldSet.toList(),
};

View File

@@ -161,7 +161,6 @@ mixin _$CheckIpSelectorState {
bool get isInit => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
num get checkIpNum => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$CheckIpSelectorStateCopyWith<CheckIpSelectorState> get copyWith =>
@@ -174,11 +173,7 @@ abstract class $CheckIpSelectorStateCopyWith<$Res> {
$Res Function(CheckIpSelectorState) then) =
_$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>;
@useResult
$Res call(
{bool isInit,
bool isStart,
Map<String, String> selectedMap,
num checkIpNum});
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
}
/// @nodoc
@@ -198,7 +193,6 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res,
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
Object? checkIpNum = null,
}) {
return _then(_value.copyWith(
isInit: null == isInit
@@ -213,10 +207,6 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res,
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
checkIpNum: null == checkIpNum
? _value.checkIpNum
: checkIpNum // ignore: cast_nullable_to_non_nullable
as num,
) as $Val);
}
}
@@ -229,11 +219,7 @@ abstract class _$$CheckIpSelectorStateImplCopyWith<$Res>
__$$CheckIpSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool isInit,
bool isStart,
Map<String, String> selectedMap,
num checkIpNum});
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
}
/// @nodoc
@@ -250,7 +236,6 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
Object? checkIpNum = null,
}) {
return _then(_$CheckIpSelectorStateImpl(
isInit: null == isInit
@@ -265,10 +250,6 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
checkIpNum: null == checkIpNum
? _value.checkIpNum
: checkIpNum // ignore: cast_nullable_to_non_nullable
as num,
));
}
}
@@ -279,8 +260,7 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
const _$CheckIpSelectorStateImpl(
{required this.isInit,
required this.isStart,
required final Map<String, String> selectedMap,
required this.checkIpNum})
required final Map<String, String> selectedMap})
: _selectedMap = selectedMap;
@override
@@ -295,12 +275,9 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
return EqualUnmodifiableMapView(_selectedMap);
}
@override
final num checkIpNum;
@override
String toString() {
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap, checkIpNum: $checkIpNum)';
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap)';
}
@override
@@ -311,14 +288,12 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
(identical(other.isInit, isInit) || other.isInit == isInit) &&
(identical(other.isStart, isStart) || other.isStart == isStart) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap) &&
(identical(other.checkIpNum, checkIpNum) ||
other.checkIpNum == checkIpNum));
.equals(other._selectedMap, _selectedMap));
}
@override
int get hashCode => Object.hash(runtimeType, isInit, isStart,
const DeepCollectionEquality().hash(_selectedMap), checkIpNum);
const DeepCollectionEquality().hash(_selectedMap));
@JsonKey(ignore: true)
@override
@@ -331,10 +306,10 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
abstract class _CheckIpSelectorState implements CheckIpSelectorState {
const factory _CheckIpSelectorState(
{required final bool isInit,
required final bool isStart,
required final Map<String, String> selectedMap,
required final num checkIpNum}) = _$CheckIpSelectorStateImpl;
{required final bool isInit,
required final bool isStart,
required final Map<String, String> selectedMap}) =
_$CheckIpSelectorStateImpl;
@override
bool get isInit;
@@ -343,8 +318,6 @@ abstract class _CheckIpSelectorState implements CheckIpSelectorState {
@override
Map<String, String> get selectedMap;
@override
num get checkIpNum;
@override
@JsonKey(ignore: true)
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
@@ -1757,37 +1730,39 @@ abstract class _ProxiesSelectorState implements ProxiesSelectorState {
}
/// @nodoc
mixin _$ProxyGroupSelectorState {
mixin _$ProxiesTabViewSelectorState {
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
ProxyCardType get proxyCardType => throw _privateConstructorUsedError;
num get sortNum => throw _privateConstructorUsedError;
List<Proxy> get proxies => throw _privateConstructorUsedError;
int get columns => throw _privateConstructorUsedError;
Group get group => throw _privateConstructorUsedError;
ViewMode get viewMode => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProxyGroupSelectorStateCopyWith<ProxyGroupSelectorState> get copyWith =>
throw _privateConstructorUsedError;
$ProxiesTabViewSelectorStateCopyWith<ProxiesTabViewSelectorState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProxyGroupSelectorStateCopyWith<$Res> {
factory $ProxyGroupSelectorStateCopyWith(ProxyGroupSelectorState value,
$Res Function(ProxyGroupSelectorState) then) =
_$ProxyGroupSelectorStateCopyWithImpl<$Res, ProxyGroupSelectorState>;
abstract class $ProxiesTabViewSelectorStateCopyWith<$Res> {
factory $ProxiesTabViewSelectorStateCopyWith(
ProxiesTabViewSelectorState value,
$Res Function(ProxiesTabViewSelectorState) then) =
_$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
ProxiesTabViewSelectorState>;
@useResult
$Res call(
{ProxiesSortType proxiesSortType,
ProxyCardType proxyCardType,
num sortNum,
List<Proxy> proxies,
int columns});
Group group,
ViewMode viewMode});
$GroupCopyWith<$Res> get group;
}
/// @nodoc
class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
$Val extends ProxyGroupSelectorState>
implements $ProxyGroupSelectorStateCopyWith<$Res> {
_$ProxyGroupSelectorStateCopyWithImpl(this._value, this._then);
class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
$Val extends ProxiesTabViewSelectorState>
implements $ProxiesTabViewSelectorStateCopyWith<$Res> {
_$ProxiesTabViewSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
@@ -1798,177 +1773,165 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res,
@override
$Res call({
Object? proxiesSortType = null,
Object? proxyCardType = null,
Object? sortNum = null,
Object? proxies = null,
Object? columns = null,
Object? group = null,
Object? viewMode = null,
}) {
return _then(_value.copyWith(
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
proxyCardType: null == proxyCardType
? _value.proxyCardType
: proxyCardType // ignore: cast_nullable_to_non_nullable
as ProxyCardType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
proxies: null == proxies
? _value.proxies
: proxies // ignore: cast_nullable_to_non_nullable
as List<Proxy>,
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
group: null == group
? _value.group
: group // ignore: cast_nullable_to_non_nullable
as Group,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$GroupCopyWith<$Res> get group {
return $GroupCopyWith<$Res>(_value.group, (value) {
return _then(_value.copyWith(group: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ProxyGroupSelectorStateImplCopyWith<$Res>
implements $ProxyGroupSelectorStateCopyWith<$Res> {
factory _$$ProxyGroupSelectorStateImplCopyWith(
_$ProxyGroupSelectorStateImpl value,
$Res Function(_$ProxyGroupSelectorStateImpl) then) =
__$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>;
abstract class _$$ProxiesTabViewSelectorStateImplCopyWith<$Res>
implements $ProxiesTabViewSelectorStateCopyWith<$Res> {
factory _$$ProxiesTabViewSelectorStateImplCopyWith(
_$ProxiesTabViewSelectorStateImpl value,
$Res Function(_$ProxiesTabViewSelectorStateImpl) then) =
__$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{ProxiesSortType proxiesSortType,
ProxyCardType proxyCardType,
num sortNum,
List<Proxy> proxies,
int columns});
Group group,
ViewMode viewMode});
@override
$GroupCopyWith<$Res> get group;
}
/// @nodoc
class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res>
extends _$ProxyGroupSelectorStateCopyWithImpl<$Res,
_$ProxyGroupSelectorStateImpl>
implements _$$ProxyGroupSelectorStateImplCopyWith<$Res> {
__$$ProxyGroupSelectorStateImplCopyWithImpl(
_$ProxyGroupSelectorStateImpl _value,
$Res Function(_$ProxyGroupSelectorStateImpl) _then)
class __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>
extends _$ProxiesTabViewSelectorStateCopyWithImpl<$Res,
_$ProxiesTabViewSelectorStateImpl>
implements _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> {
__$$ProxiesTabViewSelectorStateImplCopyWithImpl(
_$ProxiesTabViewSelectorStateImpl _value,
$Res Function(_$ProxiesTabViewSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? proxiesSortType = null,
Object? proxyCardType = null,
Object? sortNum = null,
Object? proxies = null,
Object? columns = null,
Object? group = null,
Object? viewMode = null,
}) {
return _then(_$ProxyGroupSelectorStateImpl(
return _then(_$ProxiesTabViewSelectorStateImpl(
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
proxyCardType: null == proxyCardType
? _value.proxyCardType
: proxyCardType // ignore: cast_nullable_to_non_nullable
as ProxyCardType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
proxies: null == proxies
? _value._proxies
: proxies // ignore: cast_nullable_to_non_nullable
as List<Proxy>,
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
group: null == group
? _value.group
: group // ignore: cast_nullable_to_non_nullable
as Group,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
));
}
}
/// @nodoc
class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState {
const _$ProxyGroupSelectorStateImpl(
class _$ProxiesTabViewSelectorStateImpl
implements _ProxiesTabViewSelectorState {
const _$ProxiesTabViewSelectorStateImpl(
{required this.proxiesSortType,
required this.proxyCardType,
required this.sortNum,
required final List<Proxy> proxies,
required this.columns})
: _proxies = proxies;
required this.group,
required this.viewMode});
@override
final ProxiesSortType proxiesSortType;
@override
final ProxyCardType proxyCardType;
@override
final num sortNum;
final List<Proxy> _proxies;
@override
List<Proxy> get proxies {
if (_proxies is EqualUnmodifiableListView) return _proxies;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_proxies);
}
final Group group;
@override
final int columns;
final ViewMode viewMode;
@override
String toString() {
return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, proxies: $proxies, columns: $columns)';
return 'ProxiesTabViewSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group, viewMode: $viewMode)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProxyGroupSelectorStateImpl &&
other is _$ProxiesTabViewSelectorStateImpl &&
(identical(other.proxiesSortType, proxiesSortType) ||
other.proxiesSortType == proxiesSortType) &&
(identical(other.proxyCardType, proxyCardType) ||
other.proxyCardType == proxyCardType) &&
(identical(other.sortNum, sortNum) || other.sortNum == sortNum) &&
const DeepCollectionEquality().equals(other._proxies, _proxies) &&
(identical(other.columns, columns) || other.columns == columns));
(identical(other.group, group) || other.group == group) &&
(identical(other.viewMode, viewMode) ||
other.viewMode == viewMode));
}
@override
int get hashCode => Object.hash(runtimeType, proxiesSortType, proxyCardType,
sortNum, const DeepCollectionEquality().hash(_proxies), columns);
int get hashCode =>
Object.hash(runtimeType, proxiesSortType, sortNum, group, viewMode);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ProxyGroupSelectorStateImplCopyWith<_$ProxyGroupSelectorStateImpl>
get copyWith => __$$ProxyGroupSelectorStateImplCopyWithImpl<
_$ProxyGroupSelectorStateImpl>(this, _$identity);
_$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl>
get copyWith => __$$ProxiesTabViewSelectorStateImplCopyWithImpl<
_$ProxiesTabViewSelectorStateImpl>(this, _$identity);
}
abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState {
const factory _ProxyGroupSelectorState(
abstract class _ProxiesTabViewSelectorState
implements ProxiesTabViewSelectorState {
const factory _ProxiesTabViewSelectorState(
{required final ProxiesSortType proxiesSortType,
required final ProxyCardType proxyCardType,
required final num sortNum,
required final List<Proxy> proxies,
required final int columns}) = _$ProxyGroupSelectorStateImpl;
required final Group group,
required final ViewMode viewMode}) = _$ProxiesTabViewSelectorStateImpl;
@override
ProxiesSortType get proxiesSortType;
@override
ProxyCardType get proxyCardType;
@override
num get sortNum;
@override
List<Proxy> get proxies;
Group get group;
@override
int get columns;
ViewMode get viewMode;
@override
@JsonKey(ignore: true)
_$$ProxyGroupSelectorStateImplCopyWith<_$ProxyGroupSelectorStateImpl>
_$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -54,7 +54,6 @@ class Profile with _$Profile {
UserInfo? userInfo,
@Default(true) bool autoUpdate,
@Default({}) SelectedMap selectedMap,
@Default({}) Set<String> unfoldSet,
}) = _Profile;
factory Profile.fromJson(Map<String, Object?> json) =>

View File

@@ -19,7 +19,6 @@ class CheckIpSelectorState with _$CheckIpSelectorState {
required bool isInit,
required bool isStart,
required SelectedMap selectedMap,
required num checkIpNum
}) = _CheckIpSelectorState;
}
@@ -100,14 +99,13 @@ class ProxiesSelectorState with _$ProxiesSelectorState {
}
@freezed
class ProxyGroupSelectorState with _$ProxyGroupSelectorState {
const factory ProxyGroupSelectorState({
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState {
const factory ProxiesTabViewSelectorState({
required ProxiesSortType proxiesSortType,
required ProxyCardType proxyCardType,
required num sortNum,
required List<Proxy> proxies,
required int columns,
}) = _ProxyGroupSelectorState;
required Group group,
required ViewMode viewMode,
}) = _ProxiesTabViewSelectorState;
}
@freezed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,17 @@ class AppStateContainer extends StatelessWidget {
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) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
@@ -40,8 +51,10 @@ class AppStateContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _updateNavigationsContainer(
child,
return _autoLaunchContainer(
_updateNavigationsContainer(
child,
),
);
}
}

View File

@@ -1,75 +1,47 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'text.dart';
class Info {
final String label;
final IconData? iconData;
final IconData iconData;
const Info({
required this.label,
this.iconData,
required this.iconData,
});
}
class InfoHeader extends StatelessWidget {
final Info info;
final List<Widget> actions;
const InfoHeader({
super.key,
required this.info,
List<Widget>? actions,
}) : actions = actions ?? const [];
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (info.iconData != null) ...[
Icon(
info.iconData,
color: Theme
.of(context)
.colorScheme
.primary,
),
const SizedBox(
width: 8,
),
],
Flexible(
child: TooltipText(
text: Text(
info.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme
.of(context)
.textTheme
.titleMedium,
),
),
),
],
Icon(
info.iconData,
color: Theme.of(context).colorScheme.primary,
),
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...actions,
],
const SizedBox(
width: 8,
),
Flexible(
child: TooltipText(
text: Text(
info.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
],
@@ -82,7 +54,6 @@ class CommonCard extends StatelessWidget {
const CommonCard({
super.key,
bool? isSelected,
this.type = CommonCardType.plain,
this.onPressed,
this.info,
this.selectWidget,
@@ -94,16 +65,10 @@ class CommonCard extends StatelessWidget {
final Widget? selectWidget;
final Widget child;
final Info? info;
final CommonCardType type;
BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) {
if (type == CommonCardType.filled) {
return BorderSide.none;
}
final colorScheme = Theme
.of(context)
.colorScheme;
final hoverColor = isSelected
final colorScheme = Theme.of(context).colorScheme;
var hoverColor = isSelected
? colorScheme.primary.toLight()
: colorScheme.primary.toLighter();
if (states.contains(WidgetState.hovered) ||
@@ -114,37 +79,24 @@ class CommonCard extends StatelessWidget {
);
}
return BorderSide(
color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
color:
isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
);
}
Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) {
final colorScheme = Theme
.of(context)
.colorScheme;
switch (type) {
case CommonCardType.plain:
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.secondaryContainer.toLittle();
}
return Theme
.of(context)
.outlinedButtonTheme
.style
?.backgroundColor
?.resolve(states);
case CommonCardType.filled:
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.surfaceContainerLow;
}
return colorScheme.surfaceContainer;
final colorScheme = Theme.of(context).colorScheme;
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.secondaryContainer.toLittle();
}
return Theme.of(context)
.outlinedButtonTheme
.style
?.backgroundColor
?.resolve(states);
}
@override
@@ -178,13 +130,17 @@ class CommonCard extends StatelessWidget {
),
),
backgroundColor: WidgetStateProperty.resolveWith(
(states) => getBackgroundColor(context, states),
(states) => getBackgroundColor(context, states),
),
side: WidgetStateProperty.resolveWith(
(states) => getBorderSide(context, states),
(states) => getBorderSide(context, states),
),
),
onPressed: onPressed,
onPressed: () {
if (onPressed != null) {
onPressed!();
}
},
child: Builder(
builder: (_) {
List<Widget> children = [];
@@ -211,10 +167,7 @@ class SelectIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: Theme
.of(context)
.colorScheme
.inversePrimary,
color: Theme.of(context).colorScheme.inversePrimary,
shape: const CircleBorder(),
child: Container(
padding: const EdgeInsets.all(4),

View File

@@ -17,17 +17,12 @@ class CommonChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (type == ChipType.delete) {
if(type == ChipType.delete){
return Chip(
avatar: avatar,
labelPadding:const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onDeleted: onPressed ?? () {},
side:
BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
labelStyle: Theme.of(context).textTheme.bodyMedium,
label: Text(label),
);
@@ -35,10 +30,6 @@ class CommonChip extends StatelessWidget {
return ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
avatar: avatar,
labelPadding:const EdgeInsets.symmetric(
vertical: 0,
horizontal: 4,
),
onPressed: onPressed ?? () {},
side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)),
labelStyle: Theme.of(context).textTheme.bodyMedium,

View File

@@ -1,4 +1,5 @@
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/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
@@ -18,7 +19,7 @@ class ClashMessageContainer extends StatefulWidget {
}
class _ClashMessageContainerState extends State<ClashMessageContainer>
with AppMessageListener {
with ClashMessageListener {
@override
Widget build(BuildContext context) {
return widget.child;
@@ -49,6 +50,24 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
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
void onRequest(Connection connection) async {
globalState.appController.appState.addRequest(connection);
@@ -56,28 +75,8 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
}
@override
void onLoaded(String groupName) {
final appController = globalState.appController;
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();
});
void onRun(String runTime) async {
// proxy?.updateStartTime();
super.onRun(runTime);
}
}

View File

@@ -1,10 +1,8 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/open_container.dart';
import 'package:flutter/material.dart';
import 'card.dart';
import 'extend_page.dart';
import 'scaffold.dart';
@@ -216,8 +214,7 @@ class ListItem<T> extends StatelessWidget {
return OpenContainer(
closedBuilder: (_, action) {
openAction() {
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) {
showExtendPage(
context,
@@ -246,8 +243,7 @@ class ListItem<T> extends StatelessWidget {
final nextDelegate = delegate as NextDelegate;
return _buildListTile(
onTab: () {
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) {
showExtendPage(
context,
@@ -323,101 +319,3 @@ 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,6 +7,7 @@ import 'package:flutter/services.dart';
class CommonScaffold extends StatefulWidget {
final Widget body;
final Widget? bottomNavigationBar;
final Widget? floatingActionButton;
final String title;
final Widget? leading;
final List<Widget>? actions;
@@ -19,6 +20,7 @@ class CommonScaffold extends StatefulWidget {
this.leading,
required this.title,
this.actions,
this.floatingActionButton,
this.automaticallyImplyLeading = true,
});
@@ -49,6 +51,8 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
set actions(List<Widget> actions) {
@@ -57,6 +61,12 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget? actions) {
if (_floatingActionButton.value != actions) {
_floatingActionButton.value = actions;
}
}
Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, {
String? title,
@@ -81,6 +91,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override
void dispose() {
_actions.dispose();
_floatingActionButton.dispose();
super.dispose();
}
@@ -89,6 +100,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
@@ -113,6 +125,13 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
floatingActionButton: widget.floatingActionButton ??
ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),

37
lib/widgets/section.dart Normal file
View File

@@ -0,0 +1,37 @@
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,4 +22,5 @@ export 'tile_container.dart';
export 'chip.dart';
export 'fade_box.dart';
export 'app_state_container.dart';
export 'text.dart';
export 'text.dart';
export 'section.dart';

View File

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

Some files were not shown because too many files have changed in this diff Show More