Compare commits

...

27 Commits

Author SHA1 Message Date
chen08209
35f7279fcb 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:04:57 +08:00
chen08209
86572cc960 Fix ipv6 error 2024-06-12 19:07:54 +08:00
chen08209
ee22709d49 Fix android udp direct error
Add ipv6 switch

Add access all selected button

Remove android low version splash
2024-06-12 18:29:58 +08:00
chen08209
0a2ad63f38 Update version 2024-06-10 19:11:04 +08:00
chen08209
2ec12c9363 Add allowBypass
Fix Android only pick .text file issues
2024-06-10 19:09:58 +08:00
chen08209
a3c2dc786c Fix search issues 2024-06-09 21:48:17 +08:00
chen08209
7acf9c6db3 Fix LoadBalance, Relay load error 2024-06-09 20:53:36 +08:00
chen08209
8074547fb4 Fix build.yml4 2024-06-09 19:56:51 +08:00
chen08209
8a01e04871 Fix build.yml3 2024-06-09 19:49:51 +08:00
chen08209
7ddcdd9828 Fix build.yml2 2024-06-09 19:49:14 +08:00
chen08209
d89ed076fd Fix build.yml 2024-06-09 19:46:05 +08:00
chen08209
f4c3b06cd5 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:25:14 +08:00
chen08209
c65746709d Add one-click update all profiles
Add expire show
2024-06-08 15:43:28 +08:00
chen08209
d2d9bdab02 Temp remove tun mode 2024-06-06 22:58:32 +08:00
chen08209
d0f8444b6d Remove macos in workflow 2024-06-06 22:29:26 +08:00
chen08209
fccabfbe27 Change go version 2024-06-06 22:04:29 +08:00
chen08209
e9bb97c6ce Update Version
Fix tun unable to open
2024-06-06 17:36:49 +08:00
chen08209
068fe14d89 Optimize delay test2 2024-06-06 17:13:32 +08:00
chen08209
43c397007c Optimize delay test
Add check ip
2024-06-06 16:35:09 +08:00
chen08209
5e801d5f5e add check ip request 2024-06-06 16:34:31 +08:00
chen08209
52d61b15fd Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
Fix edit profile error
2024-06-06 10:15:51 +08:00
chen08209
fea3c14608 Fix quickStart change proxy error 2024-06-05 17:59:53 +08:00
chen08209
84be01a38a Fix core version 2024-06-05 17:59:50 +08:00
chen08209
93da242148 Fix core version 2024-06-05 14:13:54 +08:00
chen08209
bb7e44da30 Update file_picker
Add resources page

Optimize more detail
2024-06-05 13:44:11 +08:00
chen08209
01f1b2d72f Add access selected sorted 2024-06-05 13:43:44 +08:00
chen08209
3074b1299e Fix notification duplicate creation issue
Fix AccessControl click issue
2024-06-03 11:48:50 +08:00
95 changed files with 35661 additions and 1849 deletions

View File

@@ -21,6 +21,12 @@ jobs:
os: macos-13
steps:
- name: Check Matrix
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
with:
@@ -82,6 +88,7 @@ jobs:
upload-release:
if: ${{ !contains(github.ref, '+') }}
permissions: write-all
needs: [ build ]
runs-on: ubuntu-latest

2
.gitignore vendored
View File

@@ -31,6 +31,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
/dist/
# Symbolication related
app.*.symbols
@@ -44,6 +45,7 @@ app.*.map.json
/android/app/release
#libclash
/libclash/

2
.gitmodules vendored
View File

@@ -5,4 +5,4 @@
[submodule "plugins/flutter_distributor"]
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git
branch = main
branch = FlClash

View File

@@ -62,7 +62,7 @@ android {
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@@ -1,9 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -17,12 +14,15 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true"
android:networkSecurityConfig="@xml/network_security_config"
android:label="FlClash">
<activity
@@ -73,7 +73,8 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:icon="@drawable/tile_icon"
android:icon="@drawable/icon"
android:foregroundServiceType="specialUse"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>

View File

@@ -1,6 +1,7 @@
package com.follow.clash
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.engine.FlutterEngine
import java.util.Date
@@ -18,6 +19,9 @@ class GlobalState {
var flutterEngine: FlutterEngine? = null
fun getCurrentTilePlugin(): TilePlugin? =
flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
fun getCurrentAppPlugin(): AppPlugin? =
flutterEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
}

View File

@@ -10,3 +10,9 @@ data class AccessControl(
val acceptList: List<String>,
val rejectList: List<String>,
)
data class Props(
val accessControl: AccessControl?,
val allowBypass: Boolean?,
val systemProxy: Boolean?,
)

View File

@@ -143,7 +143,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private suspend fun getPackages(): String {
return withContext(Dispatchers.Default){
return withContext(Dispatchers.Default) {
val packageManager = context?.packageManager
val packages: List<Package>? =
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
@@ -162,6 +162,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
fun requestGc() {
channel.invokeMethod("gc",null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
scope = CoroutineScope(Dispatchers.Default)

View File

@@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.AccessControl
import com.follow.clash.models.Props
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
@@ -41,7 +42,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
private var flClashVpnService: FlClashVpnService? = null
private var isBound = false
private var port: Int? = null
private var accessControl: AccessControl? = null
private var props: Props? = null
private lateinit var title: String
private lateinit var content: String
@@ -73,8 +74,8 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
"StartProxy" -> {
port = call.argument<Int>("port")
val args = call.argument<String>("args")
accessControl =
if (args != null) Gson().fromJson(args, AccessControl::class.java) else null
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
handleStartVpn()
result.success(true)
}
@@ -121,7 +122,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
private fun startVpn(port: Int) {
if (GlobalState.runState.value == RunState.START) return;
flClashVpnService?.start(port, accessControl)
flClashVpnService?.start(port, props)
GlobalState.runState.value = RunState.START
GlobalState.runTime = Date()
startAfter()

View File

@@ -19,7 +19,6 @@ 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 ->
@@ -43,19 +42,27 @@ class FlClashTileService : TileService() {
GlobalState.runState.observeForever(observer)
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= 34) {
val pendingIntent = PendingIntent.getActivity(
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
startActivityAndCollapse(pendingIntent)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
}else{
startActivityAndCollapse(intent)
}
}

View File

@@ -1,10 +1,9 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.ProxyInfo
@@ -13,11 +12,11 @@ import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.models.AccessControl
import com.follow.clash.R
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.Props
class FlClashVpnService : VpnService() {
@@ -52,12 +51,12 @@ class FlClashVpnService : VpnService() {
return START_STICKY
}
fun start(port: Int, accessControl: AccessControl?) {
fun start(port: Int, props: Props?) {
fd = with(Builder()) {
addAddress("172.16.0.1", 30)
setMtu(9000)
addRoute("0.0.0.0", 0)
if (accessControl != null) {
props?.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
@@ -78,8 +77,10 @@ class FlClashVpnService : VpnService() {
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
allowBypass()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (props?.allowBypass == true) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
@@ -97,55 +98,69 @@ class FlClashVpnService : VpnService() {
stopForeground()
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true);
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
}
fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
val icon = IconCompat.createWithResource(this, this.applicationInfo.icon)
val notification = with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(icon)
setContentTitle(title)
setContentText(content)
foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
setContentIntent(pendingIntent)
setOngoing(true)
setShowWhen(false)
build()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
private fun stopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(Service.STOP_FOREGROUND_REMOVE)
stopForeground(STOP_FOREGROUND_REMOVE)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -4,7 +4,6 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -4,7 +4,6 @@
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@mipmap/ic_launcher_foreground</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

BIN
assets/data/ASN.mmdb Normal file

Binary file not shown.

31589
assets/data/GeoSite.dat Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -10,6 +10,7 @@ import (
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
@@ -22,6 +23,7 @@ import (
"strings"
"sync"
"syscall"
"time"
)
type healthCheckSchema struct {
@@ -93,6 +95,13 @@ type Now struct {
Value string `json:"value"`
}
type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
UpdateAt time.Time `json:"update-at"`
}
func restartExecutable(execPath string) {
var err error
executor.Shutdown()
@@ -313,21 +322,26 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig, compatible bool) {
targetConfig.ExternalController = ""
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
//targetConfig.IPv6 = patchConfig.IPv6
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
targetConfig.GeodataMode = false
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = process.FindProcessAlways
targetConfig.AllowLan = patchConfig.AllowLan
targetConfig.MixedPort = patchConfig.MixedPort
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.GeodataLoader = "standard"
//targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
//targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
if targetConfig.DNS.Enable == false {
targetConfig.DNS = patchConfig.DNS
@@ -342,7 +356,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
}
}
func patchConfig(general *config.General) {
@@ -400,7 +413,7 @@ func applyConfig(isPatch bool) {
if isPatch {
patchConfig(cfg.General)
} else {
executor.ApplyConfig(cfg, true)
healthcheck()
hub.UltraApplyConfig(cfg, true)
hcCompatibleProvider(tunnel.Providers())
}
}

View File

@@ -8,7 +8,7 @@ require (
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
github.com/metacubex/mihomo v1.17.1
github.com/miekg/dns v1.1.59
golang.org/x/net v0.24.0
golang.org/x/net v0.25.0
golang.org/x/sync v0.7.0
)
@@ -17,11 +17,12 @@ require (
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cilium/ebpf v0.12.3 // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.7.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
@@ -30,12 +31,15 @@ require (
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.2 // indirect
github.com/gofrs/uuid/v5 v5.1.0 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.2.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
@@ -51,14 +55,15 @@ require (
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98 // indirect
github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d // indirect
github.com/metacubex/quic-go v0.43.2-0.20240518033621-2c3d14c6b38e // indirect
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
github.com/metacubex/sing-shadowsocks v0.2.6 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect
github.com/metacubex/sing-tun v0.2.6 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240512075008-89e7c6208eec // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 // indirect
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -75,10 +80,9 @@ require (
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/utls v1.5.4 // indirect
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.3 // indirect
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
@@ -94,14 +98,14 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.2 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

View File

@@ -9,6 +9,8 @@ github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -21,8 +23,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -46,6 +48,12 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -57,10 +65,10 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk=
github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -105,22 +113,24 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98 h1:oMLlJV4a9AylNo8ZLBNUiqZ02Vme6GLLHjuWJz8amSk=
github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98/go.mod h1:iGx3Y1zynls/FjFgykLSqDcM81U0IKePRTXEz5g3iiQ=
github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d h1:RAe0ND8J5SOPGI623oEXfaHKaaUrrzHx+U1DN9Awcco=
github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d/go.mod h1:WyY0zYxv+o+18R/Ece+QFontlgXoobKbNqbtYn2zjz8=
github.com/metacubex/quic-go v0.43.2-0.20240518033621-2c3d14c6b38e h1:Nzwe08FNIJpExWpy9iXkG336dN/8nJqn69yijB7vJ8g=
github.com/metacubex/quic-go v0.43.2-0.20240518033621-2c3d14c6b38e/go.mod h1:uXHODgJFUfUnkkCMWLd5Er6L5QY/LFRZb9LD5jyyhsk=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ=
github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg=
github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A=
github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8=
github.com/metacubex/sing-tun v0.2.6 h1:frc58BqnIClqcC9KcYBfVAn5bgO6WW1ANKvZW3/HYAQ=
github.com/metacubex/sing-tun v0.2.6/go.mod h1:4VsMwZH1IlgPGFK1ZbBomZ/B2MYkTgs2+gnBAr5GOIo=
github.com/metacubex/sing-tun v0.2.7-0.20240512075008-89e7c6208eec h1:K4Wq3GOdLZ/xcqwyzAt4kmYQrjokyKQ3u/Xh5Yft14U=
github.com/metacubex/sing-tun v0.2.7-0.20240512075008-89e7c6208eec/go.mod h1:4VsMwZH1IlgPGFK1ZbBomZ/B2MYkTgs2+gnBAr5GOIo=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 h1:AGyIB55UfQm/0ZH0HtQO9u3l//yjtHUpjeRjjPGfGRI=
github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
@@ -166,14 +176,12 @@ github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnV
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co=
github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -223,18 +231,18 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -254,28 +262,28 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/blake3 v1.2.2 h1:wEAbSg0IVU4ih44CVlpMqMZMpzr5hf/6aqodLlevd/w=
lukechampine.com/blake3 v1.2.2/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=

View File

@@ -10,10 +10,13 @@ import (
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context"
@@ -57,11 +60,26 @@ func shutdownClash() bool {
return true
}
//export forceGc
func forceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}
//export validateConfig
func validateConfig(s *C.char) bool {
bytes := []byte(C.GoString(s))
_, err := config.UnmarshalRawConfig(bytes)
return err == nil
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
go func() {
bytes := []byte(C.GoString(s))
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export updateConfig
@@ -171,7 +189,8 @@ func getTraffic() *C.char {
}
//export asyncTestDelay
func asyncTestDelay(s *C.char) {
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
go func() {
paramsString := C.GoString(s)
var params = &TestDelayParams{}
@@ -195,26 +214,25 @@ func asyncTestDelay(s *C.char) {
Name: params.ProxyName,
}
message := bridge.Message{
Type: bridge.Delay,
Data: delayData,
}
if proxy == nil {
delayData.Value = -1
bridge.SendMessage(message)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return
}
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if err != nil || delay == 0 {
delayData.Value = -1
bridge.SendMessage(message)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return
}
delayData.Value = int32(delay)
bridge.SendMessage(message)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return
}()
}
@@ -222,7 +240,7 @@ func asyncTestDelay(s *C.char) {
func getVersionInfo() *C.char {
versionInfo := map[string]string{
"clashName": constant.Name,
"version": constant.Version,
"version": "1.18.5",
}
data, err := json.Marshal(versionInfo)
if err != nil {
@@ -286,9 +304,82 @@ func getProvider(name *C.char) *C.char {
return C.CString(string(data))
}
//export healthcheck
func healthcheck() {
hcCompatibleProvider(tunnel.Providers())
//export getExternalProviders
func getExternalProviders() *C.char {
externalProviders := make([]ExternalProvider, 0)
providers := tunnel.Providers()
for n, p := range providers {
if p.VehicleType() != cp.Compatible {
p := p.(*provider.ProxySetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
}
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
p := p.(*rp.RuleSetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
}
data, err := json.Marshal(externalProviders)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
i := int64(port)
go func() {
providerNameString := C.GoString(providerName)
providerTypeString := C.GoString(providerType)
switch providerTypeString {
case "Proxy":
providers := tunnel.Providers()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "Rule":
providers := tunnel.RuleProviders()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
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
}
}
bridge.SendToPort(i, "")
}()
}
//export initNativeApiBridge

View File

@@ -17,36 +17,35 @@ var tunLock sync.Mutex
var tun *t.Tun
//export startTUN
func startTUN(fd C.int) bool {
tunLock.Lock()
defer tunLock.Unlock()
func startTUN(fd C.int) {
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
dns := "0.0.0.0"
if tun != nil {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
dns := "0.0.0.0"
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
closer, err := t.Start(f, gateway, portal, dns)
closer, err := t.Start(f, gateway, portal, dns)
applyConfig(true)
applyConfig(true)
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
return false
}
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
}
tempTun.Closer = closer
tempTun.Closer = closer
tun = tempTun
return true
tun = tempTun
}()
}
//export updateMarkSocketPort
@@ -61,14 +60,16 @@ func updateMarkSocketPort(markSocketPort C.longlong) bool {
//export stopTun
func stopTun() {
tunLock.Lock()
defer tunLock.Unlock()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
tun.Close()
applyConfig(true)
tun = nil
}
if tun != nil {
tun.Close()
applyConfig(true)
tun = nil
}
}()
}
func init() {

View File

@@ -82,8 +82,7 @@ class ApplicationState extends State<Application> {
super.initState();
globalState.appController = AppController(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
globalState.appController.updateViewWidth();
globalState.appController.afterInit();
await globalState.appController.init();
globalState.appController.initLink();
_updateGroups();
});
@@ -161,8 +160,9 @@ class ApplicationState extends State<Application> {
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
pageTransitionsTheme: _pageTransitionsTheme,
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
@@ -171,6 +171,7 @@ class ApplicationState extends State<Application> {
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: '',
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,

View File

@@ -5,9 +5,9 @@ import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import '../enum/enum.dart';
import '../models/models.dart';
import '../common/common.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'generated/clash_ffi.dart';
class ClashCore {
@@ -58,11 +58,20 @@ class ClashCore {
bool get isInit => clashFFI.getIsInit() == 1;
bool validateConfig(String data) {
return clashFFI.validateConfig(
data.toNativeUtf8().cast(),
) ==
1;
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.validateConfig(
data.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
return completer.future;
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
@@ -107,53 +116,76 @@ class ClashCore {
});
}
Future<DelayMap> getDelayMap() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
return Isolate.run<DelayMap>(() {
final proxies = json.decode(proxiesRawString) as Map<String, dynamic>;
return proxies.map<String, int?>(
(k, v) {
final history = v["history"] as List<dynamic>;
if (history.isEmpty) {
return MapEntry(
k,
null,
);
} else {
final delay = history.last["delay"];
return MapEntry(
k,
delay != 0 ? delay : -1,
);
}
},
);
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
}
Future<String> updateExternalProvider({
required String providerName,
required String providerType,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.updateExternalProvider(
providerName.toNativeUtf8().cast(),
providerType.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
return completer.future;
}
bool changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
return clashFFI.changeProxy(params.toNativeUtf8().cast()) == 1;
}
bool delay(String proxyName) {
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
clashFFI.asyncTestDelay(json.encode(delayParams).toNativeUtf8().cast());
return true;
final completer = Completer<Delay>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(Delay.fromJson(json.decode(message)));
receiver.close();
}
});
clashFFI.asyncTestDelay(
json.encode(delayParams).toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
Future.delayed(httpTimeoutDuration + moreDuration, () {
receiver.close();
completer.complete(
Delay(name: proxyName, value: -1),
);
});
return completer.future;
}
clearEffect(String path) {
clashFFI.clearEffect(path.toNativeUtf8().cast());
}
healthcheck() {
clashFFI.healthcheck();
}
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
@@ -178,6 +210,10 @@ class ClashCore {
clashFFI.startTUN(fd);
}
requestGc(){
clashFFI.forceGc();
}
void stopTun() {
clashFFI.stopTun();
}

View File

@@ -893,19 +893,30 @@ class ClashFFI {
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
int validateConfig(
void forceGc() {
return _forceGc();
}
late final _forceGcPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void validateConfig(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _validateConfig(
s,
port,
);
}
late final _validateConfigPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
'validateConfig');
late final _validateConfig =
_validateConfigPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
late final _validateConfigPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
late final _validateConfig = _validateConfigPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void updateConfig(
ffi.Pointer<ffi.Char> s,
@@ -974,17 +985,20 @@ class ClashFFI {
void asyncTestDelay(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _asyncTestDelay(
s,
port,
);
}
late final _asyncTestDelayPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'asyncTestDelay');
late final _asyncTestDelay =
_asyncTestDelayPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
late final _asyncTestDelayPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
late final _asyncTestDelay = _asyncTestDelayPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getVersionInfo() {
return _getVersionInfo();
@@ -1054,13 +1068,34 @@ class ClashFFI {
late final _getProvider = _getProviderPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void healthcheck() {
return _healthcheck();
ffi.Pointer<ffi.Char> getExternalProviders() {
return _getExternalProviders();
}
late final _healthcheckPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('healthcheck');
late final _healthcheck = _healthcheckPtr.asFunction<void Function()>();
late final _getExternalProvidersPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getExternalProviders');
late final _getExternalProviders =
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerType,
int port,
) {
return _updateExternalProvider(
providerName,
providerType,
port,
);
}
late final _updateExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
@@ -1095,7 +1130,7 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
int startTUN(
void startTUN(
int fd,
) {
return _startTUN(
@@ -1104,8 +1139,8 @@ class ClashFFI {
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Int)>>('startTUN');
late final _startTUN = _startTUNPtr.asFunction<int Function(int)>();
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>('startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int)>();
int updateMarkSocketPort(
int markSocketPort,

View File

@@ -1,24 +1,41 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'core.dart';
class ClashService {
Future<bool> initMmdb() async {
final mmdbPath = await appPath.getMMDBPath();
var mmdbFile = File(mmdbPath);
final isExists = await mmdbFile.exists();
if (isExists) return true;
Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoSiteFileName,
asnFileName,
];
try {
mmdbFile = await mmdbFile.create(recursive: true);
ByteData data = await rootBundle.load('assets/data/geoip.metadb');
List<int> bytes = data.buffer.asUint8List();
await mmdbFile.writeAsBytes(bytes, flush: true);
return true;
} catch (_) {
return false;
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
} catch (e) {
debugPrint("$e");
exit(0);
}
}
@@ -26,8 +43,7 @@ class ClashService {
required ClashConfig clashConfig,
required Config config,
}) async {
final isInitMmdb = await initMmdb();
if (!isInitMmdb) return false;
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final isInit = clashCore.init(homeDirPath);
return isInit;

View File

@@ -9,6 +9,8 @@ const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb";
const geoSiteFileName = "GeoSite.dat";
const asnFileName = "ASN.mmdb";
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
@@ -16,8 +18,11 @@ const configKey = "config";
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
final filter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,

View File

@@ -1,5 +1,7 @@
import 'package:fl_clash/common/app_localizations.dart';
extension DateTimeExtension on DateTime {
bool isBeforeNow() {
bool get isBeforeNow {
return isBefore(DateTime.now());
}
@@ -9,4 +11,32 @@ extension DateTimeExtension on DateTime {
}
return true;
}
}
String get lastUpdateTimeDesc {
final currentDateTime = DateTime.now();
final difference = currentDateTime.difference(this);
final days = difference.inDays;
if (days >= 365) {
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}";
}
if (days >= 30) {
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}";
}
if (days >= 1) {
return "$days ${appLocalizations.days}${appLocalizations.ago}";
}
final hours = difference.inHours;
if (hours >= 1) {
return "$hours ${appLocalizations.hours}${appLocalizations.ago}";
}
final minutes = difference.inMinutes;
if (minutes >= 1) {
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}";
}
return appLocalizations.just;
}
String get show {
return toIso8601String().substring(0, 10);
}
}

View File

@@ -23,10 +23,11 @@ class Measure {
double? _bodyMediumHeight;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
double? _titleLargeHeight;
double? _titleMediumHeight;
double get bodyMediumHeight{
double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize(
Text(
"",
@@ -36,7 +37,7 @@ class Measure {
return _bodyMediumHeight!;
}
double get bodySmallHeight{
double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize(
Text(
"",
@@ -46,7 +47,7 @@ class Measure {
return _bodySmallHeight!;
}
double get labelSmallHeight{
double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize(
Text(
"",
@@ -56,7 +57,17 @@ class Measure {
return _labelSmallHeight!;
}
double get titleLargeHeight{
double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize(
Text(
"",
style: context.textTheme.labelMedium,
),
).height;
return _labelMediumHeight!;
}
double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize(
Text(
"",
@@ -65,4 +76,14 @@ class Measure {
).height;
return _titleLargeHeight!;
}
double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize(
Text(
"",
style: context.textTheme.titleMedium,
),
).height;
return _titleMediumHeight!;
}
}

View File

@@ -29,6 +29,14 @@ class Navigation {
label: "profiles",
fragment: ProfilesFragment(),
),
const NavigationItem(
icon: Icon(Icons.swap_vert_circle),
label: "resources",
description: "resourcesDesc",
keep: false,
fragment: Resources(),
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
NavigationItem(
icon: const Icon(Icons.adb),
label: "logs",

View File

@@ -2,6 +2,8 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
@@ -178,6 +180,12 @@ class Other {
.where((item) => item.isNotEmpty)
.toList();
}
ViewMode getViewMode(double viewWidth){
if (viewWidth <= maxMobileWidth) return ViewMode.mobile;
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop;
}
}
final other = Other();

View File

@@ -36,11 +36,6 @@ class AppPath {
final directory = await getProfilesPath();
return join(directory, "$id.yaml");
}
Future<String> getMMDBPath() async {
var directory = await applicationSupportDirectoryCompleter.future;
return join(directory.path, mmdbFileName);
}
}
final appPath = AppPath();

View File

@@ -3,16 +3,14 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart';
import 'package:fl_clash/models/models.dart';
class Picker {
Future<Result<PlatformFile>> pickerConfigFile() async {
Future<PlatformFile?> pickerConfigFile() async {
FilePickerResult? filePickerResult;
if (Platform.isAndroid) {
filePickerResult = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.custom,
allowedExtensions: ['txt', 'conf'],
allowMultiple: false,
);
} else {
filePickerResult = await FilePicker.platform.pickFiles(
@@ -23,20 +21,20 @@ class Picker {
}
final file = filePickerResult?.files.first;
if (file == null) {
return Result.error(appLocalizations.pleaseUploadFile);
return null;
}
return Result.success(file);
return file;
}
Future<Result<String>> pickerConfigQRCode() async {
Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
final bytes = await xFile?.readAsBytes();
if (bytes == null) return Result.error();
if (bytes == null) return null;
final result = await other.parseQRCode(bytes);
if (result == null || !result.isUrl) {
return Result.error(appLocalizations.pleaseUploadValidQrcode);
throw appLocalizations.pleaseUploadValidQrcode;
}
return Result.success(result);
return result;
}
}

View File

@@ -11,7 +11,7 @@ class ProxyManager {
_proxy = proxy ?? proxy_plugin.Proxy();
}
bool get isStart => startTime != null && startTime!.isBeforeNow();
bool get isStart => startTime != null && startTime!.isBeforeNow;
DateTime? get startTime => _proxy.startTime;

View File

@@ -1,36 +1,105 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart';
import 'package:http/http.dart';
import '../models/models.dart';
import 'package:fl_clash/models/ip.dart';
import 'package:fl_clash/state.dart';
class Request {
static Future<Result<Response>> getFileResponseForUrl(String url) async {
final headers = {'User-Agent': coreName};
try {
final response = await get(Uri.parse(url), headers: headers).timeout(
httpTimeoutDuration,
late final Dio _dio;
int? _port;
bool _isStart = false;
Request() {
_dio = Dio(
BaseOptions(
headers: {"User-Agent": coreName},
),
);
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
return handler.next(options); // 继续请求
},
));
}
_syncProxy() {
final port = globalState.appController.clashConfig.mixedPort;
final isStart = globalState.appController.appState.isStart;
if (_port != port || isStart != _isStart) {
_port = port;
_isStart = isStart;
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
if (!_isStart) return client;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
return client;
},
);
return Result.success(response);
} catch (err) {
return Result.error(err.toString());
}
}
static Future<Result<Map<String,dynamic>>> checkForUpdate() async {
final response = await get(
Uri.parse(
"https://api.github.com/repos/$repository/releases/latest",
Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio
.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration * 2,
);
return response;
}
Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await _dio.get(
"https://api.github.com/repos/$repository/releases/latest",
options: Options(
responseType: ResponseType.json,
),
);
if (response.statusCode != 200) return Result.error();
final body = json.decode(response.body) as Map<String,dynamic>;
final remoteVersion = body['tag_name'];
if (response.statusCode != 200) return null;
final data = response.data as Map<String, dynamic>;
final remoteVersion = data['tag_name'];
final packageInfo = await appPackage.packageInfoCompleter.future;
final version = packageInfo.version;
final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return Result.error();
return Result.success(body);
if (!hasUpdate) return null;
return data;
}
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp(CancelToken? cancelToken) async {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.timeout(
httpTimeoutDuration,
);
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
}
} catch (e) {
continue;
}
}
return null;
}
}
final request = Request();

View File

@@ -6,6 +6,11 @@ extension TextStyleExtension on TextStyle {
return copyWith(color: color?.toLight());
}
toLighter() {
return copyWith(color: color?.toLighter());
}
toSoftBold() {
return copyWith(fontWeight: FontWeight.w500);
}

View File

@@ -1,12 +1,12 @@
import 'dart:async';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'clash/core.dart';
import 'enum/enum.dart';
import 'models/models.dart';
import 'common/common.dart';
@@ -40,8 +40,6 @@ class AppController {
updateRunTime,
updateTraffic,
];
clearShowProxyDelay();
testShowProxyDelay();
} else {
await globalState.stopSystemProxy();
appState.traffics = [];
@@ -78,10 +76,10 @@ class AppController {
);
}
addProfile(Profile profile) {
addProfile(Profile profile) async {
config.setProfile(profile);
if (config.currentProfileId != null) return;
changeProfile(profile.id);
await changeProfile(profile.id);
}
deleteProfile(String id) async {
@@ -99,18 +97,17 @@ class AppController {
}
}
updateProfile(String id) async {
Future<void> updateProfile(String id) async {
final profile = config.getCurrentProfileForId(id);
if (profile != null) {
final res = await profile.update();
if (res.type == ResultType.success) {
config.setProfile(profile);
}
final tempProfile = profile.copyWith();
await tempProfile.update();
config.setProfile(tempProfile);
}
}
Future<String> updateClashConfig({bool isPatch = true}) async {
return await globalState.updateClashConfig(
Future<void> updateClashConfig({bool isPatch = true}) async {
await globalState.updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
@@ -118,24 +115,15 @@ class AppController {
}
Future applyProfile() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
Function? _changeProfileDebounce;
changeProfileDebounce(String? profileId) {
if (profileId == config.currentProfileId) return;
config.currentProfileId = profileId;
_changeProfileDebounce ??= debounce<Function(String?)>((profileId) async {
await applyProfile();
appState.delayMap = {};
saveConfigPreferences();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
});
_changeProfileDebounce!([profileId]);
}
changeProfile(String? value) async {
@@ -148,14 +136,25 @@ class AppController {
autoUpdateProfiles() async {
for (final profile in config.profiles) {
if (!profile.autoUpdate) return;
if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate
?.add(
profile.autoUpdateDuration,
)
.isBeforeNow();
if (isNotNeedUpdate == false) continue;
await profile.update();
.isBeforeNow;
if (isNotNeedUpdate == false || profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
}
}
updateProfiles() async {
for (final profile in config.profiles) {
if (profile.type == ProfileType.file) {
continue;
}
await updateProfile(profile.id);
}
}
@@ -211,18 +210,17 @@ class AppController {
autoCheckUpdate() async {
if (!config.autoCheckUpdate) return;
final res = await Request.checkForUpdate();
checkUpdateResultHandle(result: res);
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
}
checkUpdateResultHandle({
Result<Map<String, dynamic>>? result,
bool handleError = false
}) async {
if (result == null) return;
if (result.type == ResultType.success) {
final tagName = result.data?['tag_name'];
final body = result.data?['body'];
Map<String, dynamic>? data,
bool handleError = false,
}) async {
if (data != null) {
final tagName = data['tag_name'];
final body = data['body'];
final submits = other.parseReleaseBody(body);
globalState.showMessage(
title: appLocalizations.discoverNewVersion,
@@ -248,7 +246,7 @@ class AppController {
},
confirmText: appLocalizations.goDownload,
);
} else if(handleError){
} else if (handleError) {
globalState.showMessage(
title: appLocalizations.checkUpdate,
message: TextSpan(
@@ -258,6 +256,29 @@ class AppController {
}
}
init() async {
if (!config.silentLaunch) {
window?.show();
}
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if(commonScaffoldState?.mounted == true){
await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
});
}else{
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
await afterInit();
}
afterInit() async {
if (config.autoRun) {
await updateSystemProxy(true);
@@ -267,34 +288,17 @@ class AppController {
}
autoUpdateProfiles();
updateLogStatus();
if (!config.silentLaunch) {
window?.show();
}
autoCheckUpdate();
}
healthcheck() {
if (globalState.healthcheckLock) return;
for (final delay in appState.delayMap.entries) {
setDelay(
Delay(
name: delay.key,
value: 0,
),
);
}
clashCore.healthcheck();
}
setDelay(Delay delay) {
appState.setDelay(delay);
}
updateDelayMap() async {
appState.delayMap = await clashCore.getDelayMap();
}
toPage(int index, {bool hasAnimate = false}) {
if (index > appState.currentNavigationItems.length - 1) {
return;
}
appState.currentLabel = appState.currentNavigationItems[index].label;
if ((config.isAnimateToPage || hasAnimate)) {
globalState.pageController?.animateToPage(
@@ -350,94 +354,55 @@ class AppController {
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(
final profile = await commonScaffoldState?.loadingRun<Profile>(
() async {
await Future.delayed(const Duration(milliseconds: 300));
final profile = Profile(
url: url,
);
final res = await profile.update();
if (res.type == ResultType.success) {
addProfile(profile);
} else {
debugPrint(res.message);
globalState.showMessage(
title: "${appLocalizations.add}${appLocalizations.profile}",
message: TextSpan(text: res.message!),
);
}
await profile.update();
return profile;
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
addProfileFormFile() async {
final result = await picker.pickerConfigFile();
if (result.type == ResultType.error) return;
final platformFile = await globalState.safeRun(picker.pickerConfigFile);
if (!context.mounted) return;
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(
final profile = await commonScaffoldState?.loadingRun<Profile?>(
() async {
await Future.delayed(const Duration(milliseconds: 300));
final bytes = result.data?.bytes;
final bytes = platformFile?.bytes;
if (bytes == null) {
return;
return null;
}
final profile = Profile(label: result.data?.name);
final sRes = await profile.saveFile(bytes);
if (sRes.type == ResultType.error) {
debugPrint(sRes.message);
globalState.showMessage(
title: "${appLocalizations.add}${appLocalizations.profile}",
message: TextSpan(text: sRes.message!),
);
return;
}
addProfile(profile);
final profile = Profile(label: platformFile?.name);
await profile.saveFile(bytes);
return profile;
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
addProfileFormQrCode() async {
final result = await picker.pickerConfigQRCode();
if (result.type == ResultType.error) {
if (result.message != null) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: result.message,
),
);
}
return;
}
addProfileFormURL(result.data!);
final url = await globalState.safeRun(picker.pickerConfigQRCode);
if (url == null) return;
addProfileFormURL(url);
}
clearShowProxyDelay() {
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
if (showProxyDelay != null) {
appState.setDelay(
Delay(name: showProxyDelay, value: null),
);
}
}
testShowProxyDelay() {
final showProxyDelay = appState.getRealProxyName(appState.showProxyName);
if (showProxyDelay != null) {
globalState.updateCurrentDelay(showProxyDelay);
}
}
updateViewWidth() {
appState.viewWidth = context.width;
if (appState.viewWidth == 0) {
Future.delayed(moreDuration, () {
updateViewWidth();
});
}
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
});
}
}

View File

@@ -1,6 +1,6 @@
// ignore_for_file: constant_identifier_names
enum GroupType { Selector, URLTest, Fallback }
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback }

View File

@@ -1,5 +1,4 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -11,13 +10,13 @@ class AboutFragment extends StatelessWidget {
_checkUpdate(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final res =
await commonScaffoldState?.loadingRun<Result<Map<String, dynamic>>>(
Request.checkForUpdate,
final data =
await commonScaffoldState?.loadingRun<Map<String, dynamic>?>(
request.checkForUpdate,
title: appLocalizations.checkUpdate,
);
globalState.appController.checkUpdateResultHandle(
result: res,
data: data,
handleError: true,
);
}

View File

@@ -1,11 +1,20 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
extension AccessControlExtension on AccessControl {
List<String> get currentList => switch (mode) {
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
}
class AccessFragment extends StatefulWidget {
const AccessFragment({super.key});
@@ -19,8 +28,10 @@ class _AccessFragmentState extends State<AccessFragment> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
packagesListenable.value = await app?.getPackages() ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 300), () async {
packagesListenable.value = await app?.getPackages() ?? [];
});
});
}
@@ -79,130 +90,110 @@ class _AccessFragmentState extends State<AccessFragment> {
);
}
Widget _buildSelectedAllButton({
Widget _buildSearchButton(List<Package> packages) {
return IconButton(
tooltip: appLocalizations.search,
onPressed: () {
showSearch(
context: context,
delegate: AccessControlSearchDelegate(
packages: packages,
),
).then((_) => {setState(() {})});
},
icon: const Icon(Icons.search),
);
}
_buildSelectedAllButton({
required bool isAccessControl,
required bool isSelectedAll,
required List<String> allValueList,
}) {
return Builder(
builder: (context) {
final tooltip = isSelectedAll
? appLocalizations.cancelSelectAll
: appLocalizations.selectAll;
return IconButton(
tooltip: tooltip,
onPressed: () {
final config = context.read<Config>();
if (isSelectedAll) {
config.accessControl.currentList = [];
config.accessControl = config.accessControl.copyWith();
} else {
config.accessControl.currentList = allValueList;
config.accessControl = config.accessControl.copyWith();
}
},
icon: isSelectedAll
? const Icon(Icons.deselect)
: const Icon(Icons.select_all),
);
},
);
}
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;
Widget _actionHeader({
required bool isAccessControl,
required List<String> valueList,
required String describe,
required List<String> packageNameList,
}) {
return 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,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color:
Theme.of(context).colorScheme.primary,
),
),
),
],
),
if (isSelectedAll) {
config.accessControl = switch (isAccept) {
true => config.accessControl.copyWith(
acceptList: [],
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSelectedAllButton(
isSelectedAll: valueList.length == packageNameList.length,
allValueList: packageNameList,
),
),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
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(bool isAccessControl) {
Widget _buildPackageList() {
return ValueListenableBuilder(
valueListenable: packagesListenable,
builder: (_, packages, ___) {
return Selector<Config, AccessControl>(
selector: (_, config) => config.accessControl,
builder: (context, accessControl, __) {
final accessControl = globalState.appController.config.accessControl;
final acceptList = accessControl.acceptList;
final rejectList = accessControl.rejectList;
final acceptPackages = packages.sorted((a, b) {
final isSelectA = acceptList.contains(a.packageName);
final isSelectB = acceptList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
final rejectPackages = packages.sorted((a, b) {
final isSelectA = rejectList.contains(a.packageName);
final isSelectB = rejectList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final isFilterSystemApp = accessControl.isFilterSystemApp;
final accessControlMode = accessControl.mode;
final packages =
accessControlMode == AccessControlMode.acceptSelected
? acceptPackages
: rejectPackages;
final currentList = accessControl.currentList;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
@@ -210,23 +201,96 @@ class _AccessFragmentState extends State<AccessFragment> {
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
final accessControlMode = accessControl.mode;
final valueList =
accessControl.currentList.intersection(packageNameList);
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
_buildSelectedAllButton(
isAccessControl: isAccessControl,
isSelectedAll: valueList.length == packageNameList.length,
allValueList: packageNameList,
);
return DisabledMask(
status: !isAccessControl,
child: Column(
children: [
_actionHeader(
isAccessControl: isAccessControl,
valueList: valueList,
describe: describe,
packageNameList: packageNameList,
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,
),
),
Flexible(
child: Text(
"${valueList.length}",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.primary,
),
),
),
],
),
),
Flexible(
child: Text(describe),
)
],
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: _buildSearchButton(currentPackages)),
Flexible(child: _buildFilterSystemAppButton()),
Flexible(child: _buildAppProxyModePopup()),
],
),
],
),
),
),
Expanded(
flex: 1,
@@ -241,7 +305,7 @@ class _AccessFragmentState extends State<AccessFragment> {
itemBuilder: (_, index) {
final package = currentPackages[index];
return PackageListItem(
key: Key(package.label),
key: Key(package.packageName),
package: package,
value:
valueList.contains(package.packageName),
@@ -252,11 +316,20 @@ class _AccessFragmentState extends State<AccessFragment> {
} else {
valueList.remove(package.packageName);
}
final config = context.read<Config>();
config.accessControl.currentList =
valueList;
config.accessControl =
config.accessControl.copyWith();
final config =
globalState.appController.config;
if (accessControlMode ==
AccessControlMode.acceptSelected) {
config.accessControl =
config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl =
config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
@@ -276,7 +349,7 @@ class _AccessFragmentState extends State<AccessFragment> {
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.isAccessControl,
builder: (_, isAccessControl, __) {
builder: (_, isAccessControl, child) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
@@ -300,11 +373,12 @@ class _AccessFragmentState extends State<AccessFragment> {
),
),
Flexible(
child: _buildPackageList(isAccessControl),
child: child!,
),
],
);
},
child: _buildPackageList(),
);
}
}
@@ -369,3 +443,111 @@ class PackageListItem extends StatelessWidget {
);
}
}
class AccessControlSearchDelegate extends SearchDelegate {
final List<Package> packages;
AccessControlSearchDelegate({
required this.packages,
});
List<Package> get _results {
final lowQuery = query.toLowerCase();
return packages
.where(
(package) =>
package.label.toLowerCase().contains(lowQuery) ||
package.packageName.contains(lowQuery),
)
.toList();
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
Widget _packageList(List<Package> packages) {
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final currentList = accessControl.currentList;
final packageNameList =
this.packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
return DisabledMask(
status: !isAccessControl,
child: ListView.builder(
itemCount: packages.length,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: isAccessControl,
onChanged: (value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
final config = globalState.appController.config;
if (accessControlMode == AccessControlMode.acceptSelected) {
config.accessControl = config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl = config.accessControl.copyWith(
rejectList: valueList,
);
}
},
);
},
),
);
},
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
Widget buildSuggestions(BuildContext context) {
return _packageList(_results);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -44,9 +46,110 @@ class _ConfigFragmentState extends State<ConfigFragment> {
globalState.appController.updateClashConfigDebounce();
}
@override
Widget build(BuildContext context) {
List<Widget> items = [
_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,
)
]
],
),
);
}
_buildGeneralSection() {
final items = [
Padding(
padding: kMaterialListPadding,
child: Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
trailing: SizedBox(
height: 48,
child: DropdownMenu<LogLevel>(
width: 124,
initialSelection: value,
dropdownMenuEntries: [
for (final logLevel in LogLevel.values)
DropdownMenuEntry<LogLevel>(
value: logLevel,
label: logLevel.name,
)
],
onSelected: _updateLoglevel,
),
),
);
},
),
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
@@ -54,9 +157,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
onTab: () {
_modifyMixedPort(mixedPort);
},
padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4),
leading: const Icon(Icons.adjust),
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
@@ -68,6 +171,24 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
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, __) {
@@ -86,92 +207,113 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
if (system.isDesktop)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
return ListItem.switchItem(
leading: const Icon(Icons.support),
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();
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.expand),
title: Text(appLocalizations.compatible),
subtitle: Text(appLocalizations.compatibleDesc),
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: isCompatible,
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCompatible = value;
await appController.updateClashConfig(isPatch: false);
await appController.updateGroups();
appController.changeProxy();
appController.clashConfig.unifiedDelay = value;
appController.updateClashConfigDebounce();
},
),
);
},
),
Padding(
padding: kMaterialListPadding,
child: Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.feedback),
title: Text(appLocalizations.logLevel),
trailing: SizedBox(
height: 48,
child: DropdownMenu<LogLevel>(
width: 124,
inputDecorationTheme: const InputDecorationTheme(
filled: true,
contentPadding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 16,
),
),
initialSelection: value,
dropdownMenuEntries: [
for (final logLevel in LogLevel.values)
DropdownMenuEntry<LogLevel>(
value: logLevel,
label: logLevel.name,
)
],
onSelected: _updateLoglevel,
),
),
);
},
),
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 ListView.separated(
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(),
];
return ListView.builder(
padding: const EdgeInsets.only(bottom: 16),
itemBuilder: (_, index) {
return Container(
alignment: Alignment.center,
child: items[index],
);
},
separatorBuilder: (_, __) {
return const Divider(
height: 0,
);
},
itemCount: items.length,
);
}

View File

@@ -1,3 +1,5 @@
import 'package:country_flags/country_flags.dart';
import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -13,118 +15,162 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State<NetworkDetection> {
Widget _buildDescription(String? currentProxyName, int? delay) {
if (currentProxyName == null) {
return TooltipText(
text: Text(
appLocalizations.noProxyDesc,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
overflow: TextOverflow.ellipsis,
),
);
final ipInfoNotifier = ValueNotifier<IpInfo?>(null);
final timeoutNotifier = ValueNotifier<bool>(false);
bool? _preIsStart;
CancelToken? cancelToken;
_checkIp(
bool isInit,
bool isStart,
) async {
if (!isInit) return;
timeoutNotifier.value = false;
if (_preIsStart == false && _preIsStart == isStart) return;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
if (delay == 0 || delay == null) {
return const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(
strokeCap: StrokeCap.round,
),
);
ipInfoNotifier.value = null;
final ipInfo = await request.checkIp(cancelToken);
if (ipInfo == null) {
timeoutNotifier.value = true;
return;
} else {
timeoutNotifier.value = false;
}
if (delay > 0) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TooltipText(
text: Text(
"$delay",
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: context.textTheme.titleLarge
?.copyWith(
color: context.colorScheme.primary,
)
.toSoftBold(),
),
),
const Flexible(
child: SizedBox(
width: 4,
),
),
Flexible(
flex: 0,
child: Text(
'ms',
style: Theme.of(context).textTheme.bodyMedium?.toLight(),
),
),
],
);
}
return Text(
"Timeout",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.red,
),
_preIsStart = isStart;
ipInfoNotifier.value = ipInfo;
}
_checkIpContainer(Widget child) {
return Selector2<AppState, Config, CheckIpSelectorState>(
selector: (_, appState, config) {
return CheckIpSelectorState(
isInit: appState.isInit,
selectedMap: appState.selectedMap,
isStart: appState.isStart,
);
},
builder: (_, state, __) {
_checkIp(state.isInit, state.isStart);
return child;
},
child: child,
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
info: Info(
iconData: Icons.network_check,
label: appLocalizations.networkDetection,
),
child: Selector<AppState, NetworkDetectionSelectorState>(
selector: (_, appState) {
return NetworkDetectionSelectorState(
currentProxyName: appState.showProxyName,
delay: appState.getDelay(
appState.showProxyName,
),
);
},
builder: (_, state, __) {
return Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
return _checkIpContainer(
ValueListenableBuilder<IpInfo?>(
valueListenable: ipInfoNotifier,
builder: (_, ipInfo, __) {
return CommonCard(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 0,
child: TooltipText(
text: Text(
state.currentProxyName ?? appLocalizations.noProxy,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleMedium
?.toSoftBold(),
),
),
),
const SizedBox(
height: 8,
),
Flexible(
child: Container(
height: globalState.appController.measure.titleLargeHeight,
alignment: Alignment.centerLeft,
child: FadeBox(
child: _buildDescription(
state.currentProxyName,
state.delay,
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.network_check,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: FadeBox(
child: ipInfo != null
? CountryFlag.fromCountryCode(
ipInfo.countryCode,
width: 24,
height: 24,
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
appLocalizations.checkError,
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return TooltipText(
text: Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium,
),
);
},
),
),
),
],
),
),
),
Container(
height:
globalState.appController.measure.titleLargeHeight + 24,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox(
child: ipInfo != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.titleLarge
?.toSoftBold(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
)
: ValueListenableBuilder(
valueListenable: timeoutNotifier,
builder: (_, timeout, __) {
if (timeout) {
return Text(
"timeout",
style: context.textTheme.titleMedium
?.copyWith(color: Colors.red)
.toSoftBold(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
);
},
),
),
)
],
),
);

View File

@@ -19,7 +19,7 @@ class _StartButtonState extends State<StartButton>
@override
void initState() {
isStart = context.read<AppState>().runTime != null;
isStart = globalState.appController.appState.isStart;
_controller = AnimationController(
vsync: this,
value: isStart ? 1 : 0,
@@ -48,9 +48,11 @@ class _StartButtonState extends State<StartButton>
}
}
updateSystemProxy() async {
final appController = globalState.appController;
await appController.updateSystemProxy(isStart);
updateSystemProxy() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final appController = globalState.appController;
await appController.updateSystemProxy(isStart);
});
}
@override

View File

@@ -8,4 +8,5 @@ export 'access.dart';
export 'config.dart';
export 'application_setting.dart';
export 'about.dart';
export 'backup_and_recovery.dart';
export 'backup_and_recovery.dart';
export 'resources.dart';

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -7,10 +8,40 @@ import 'package:provider/provider.dart';
import '../models/models.dart';
import '../widgets/widgets.dart';
class LogsFragment extends StatelessWidget {
class LogsFragment extends StatefulWidget {
const LogsFragment({super.key});
_initActions(BuildContext context) {
@override
State<LogsFragment> createState() => _LogsFragmentState();
}
class _LogsFragmentState extends State<LogsFragment> {
final logsNotifier = ValueNotifier<List<Log>>([]);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
logsNotifier.value = context.read<AppState>().logs;
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
logsNotifier.value = globalState.appController.appState.logs;
});
});
}
@override
void dispose() {
super.dispose();
timer?.cancel();
timer = null;
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
@@ -20,21 +51,22 @@ class LogsFragment extends StatelessWidget {
showSearch(
context: context,
delegate: LogsSearchDelegate(
logs: globalState.appController.appState.logs.reversed.toList(),
logs: logsNotifier.value.reversed.toList(),
),
);
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
});
}
_buildList() {
return Selector<AppState, List<Log>>(
selector: (_, appState) => appState.logs,
shouldRebuild: (prev, next) =>
!const ListEquality<Log>().equals(prev, next),
return ValueListenableBuilder<List<Log>>(
valueListenable: logsNotifier,
builder: (_, List<Log> logs, __) {
if (logs.isEmpty) {
return NullStatus(
@@ -48,7 +80,6 @@ class LogsFragment extends StatelessWidget {
itemBuilder: (BuildContext context, int index) {
final log = logs[index];
return LogItem(
key: ValueKey(log.dateTime),
log: log,
);
},
@@ -65,14 +96,13 @@ class LogsFragment extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) {
return appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools";
},
selector: (_, appState) =>
appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions(context);
_initActions();
}
return child!;
},
@@ -112,6 +142,9 @@ class LogsSearchDelegate extends SearchDelegate {
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}

View File

@@ -1,4 +1,5 @@
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';
@@ -40,7 +41,7 @@ class _EditProfileState extends State<EditProfile> {
_handleConfirm() {
if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>();
final hasUpdate = widget.profile.url != urlController.text;
final hasUpdate = urlController.text.isNotEmpty && widget.profile.url != urlController.text;
widget.profile.url = urlController.text;
widget.profile.label = labelController.text;
widget.profile.autoUpdate = autoUpdate;
@@ -82,7 +83,7 @@ class _EditProfileState extends State<EditProfile> {
},
),
),
if (widget.profile.url != null)...[
if (widget.profile.type == ProfileType.url)...[
ListItem(
title: TextFormField(
controller: urlController,
@@ -151,6 +152,7 @@ class _EditProfileState extends State<EditProfile> {
vertical: 16,
),
child: ListView.separated(
primary: true,
itemBuilder: (_, index) {
return items[index];
},

View File

@@ -25,16 +25,7 @@ class ProfilesFragment extends StatefulWidget {
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
_handleUpdateProfile(String id) async {
context.findAncestorStateOfType<CommonScaffoldState>()?.loadingRun(
() => globalState.appController.updateProfile(id),
);
}
final hasPadding = ValueNotifier<bool>(false);
_handleShowAddExtendPage() {
showExtendPage(
@@ -46,76 +37,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
}
_handleShowEditExtendPage(Profile profile) {
showExtendPage(
context,
body: EditProfile(
profile: profile.copyWith(),
context: context,
),
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
_buildGrid({
required ProfilesSelectorState state,
int crossAxisCount = 1,
}) {
return SingleChildScrollView(
padding: crossAxisCount > 1
? const EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
child: Grid.baseGap(
crossAxisCount: crossAxisCount,
children: [
for (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
commonPopupMenu: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
),
if (profile.url != null)
CommonPopupMenuItem(
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage(profile);
break;
case ProfileActions.delete:
_handleDeleteProfile(profile.id);
break;
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case null:
break;
}
},
),
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
);
}
_getColumns(ViewMode viewMode) {
switch (viewMode) {
case ViewMode.mobile:
@@ -127,33 +48,104 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
@override
Widget build(BuildContext context) {
return FloatLayout(
floatingWidget: Container(
margin: const EdgeInsets.all(kFloatingActionButtonMargin),
child: FloatingActionButton(
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
commonScaffoldState.loadingRun<void>(
() async {
await globalState.appController.updateProfiles();
},
);
},
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
)
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(Icons.add),
),
),
child: const Icon(
Icons.add,
),
);
},
);
}
@override
Widget build(BuildContext context) {
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),
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
final columns = _getColumns(state.viewMode);
final isMobile = state.viewMode == ViewMode.mobile;
return Align(
alignment: Alignment.topCenter,
child: _buildGrid(
state: state,
crossAxisCount: _getColumns(state.viewMode),
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 (final profile in state.profiles)
GridItem(
child: ProfileItem(
profile: profile,
groupValue: state.currentProfileId,
onChanged:
globalState.appController.changeProfile,
),
),
],
),
);
},
),
),
);
},
@@ -162,118 +154,188 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
class ProfileItem extends StatelessWidget {
class ProfileItem extends StatefulWidget {
final Profile profile;
final String? groupValue;
final CommonPopupMenu commonPopupMenu;
final void Function(String? value) onChanged;
const ProfileItem({
super.key,
required this.profile,
required this.commonPopupMenu,
required this.groupValue,
required this.onChanged,
});
String _getLastUpdateTimeDifference(DateTime lastDateTime) {
final currentDateTime = DateTime.now();
final difference = currentDateTime.difference(lastDateTime);
final days = difference.inDays;
if (days >= 365) {
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}";
}
if (days >= 30) {
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}";
}
if (days >= 1) {
return "$days ${appLocalizations.days}${appLocalizations.ago}";
}
final hours = difference.inHours;
if (hours >= 1) {
return "$hours ${appLocalizations.hours}${appLocalizations.ago}";
}
final minutes = difference.inMinutes;
if (minutes >= 1) {
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}";
}
return appLocalizations.just;
@override
State<ProfileItem> createState() => _ProfileItemState();
}
class _ProfileItemState extends State<ProfileItem> {
final isUpdating = ValueNotifier<bool>(false);
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
@override
Widget build(BuildContext context) {
String useShow;
String totalShow;
double progress;
final userInfo = profile.userInfo;
if (userInfo == null) {
useShow = "Infinite";
totalShow = "Infinite";
progress = 1;
} else {
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
useShow = TrafficValue(value: use).show;
totalShow = TrafficValue(value: total).show;
progress = total == 0 ? 0.0 : use / total;
}
return ListItem.radio(
horizontalTitleGap: 16,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
_handleUpdateProfile(String id) async {
isUpdating.value = true;
await globalState.safeRun<void>(() async {
await globalState.appController.updateProfile(id);
});
isUpdating.value = false;
}
_handleShowEditExtendPage(
Profile profile,
) {
showExtendPage(
context,
body: EditProfile(
profile: profile.copyWith(),
context: context,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: commonPopupMenu,
title: Column(
mainAxisSize: MainAxisSize.min,
title: "${appLocalizations.edit}${appLocalizations.profile}",
);
}
_buildTitle(Profile profile) {
final textTheme = context.textTheme;
final userInfo = profile.userInfo ?? UserInfo();
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
final useShow = TrafficValue(value: use).show;
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? "长期有效"
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
profile.label ?? profile.id,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
profile.lastUpdateDate != null
? _getLastUpdateTimeDifference(profile.lastUpdateDate!)
: '',
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
),
],
),
),
Flexible(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
profile.label ?? profile.id,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: textTheme.labelMedium?.toLight(),
),
),
],
),
Flexible(
child: Text(
"$useShow / $totalShow",
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: LinearProgressIndicator(
minHeight: 6,
value: progress,
),
),
Text(
"$useShow / $totalShow",
style: textTheme.labelMedium?.toLight(),
),
const SizedBox(
height: 2,
),
Row(
children: [
Text(
"到期时间:",
style: textTheme.labelMedium?.toLighter(),
),
const SizedBox(
width: 4,
),
Text(
expireShow,
style: textTheme.labelMedium?.toLighter(),
),
],
)
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
final profile = widget.profile;
final groupValue = widget.groupValue;
final onChanged = widget.onChanged;
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,
delegate: RadioDelegate<String?>(
value: profile.id,
groupValue: groupValue,
onChanged: onChanged,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
trailing: CommonPopupMenu<ProfileActions>(
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,
label: appLocalizations.edit,
iconData: Icons.edit,
),
if (profile.type == ProfileType.url)
CommonPopupMenuItem(
action: ProfileActions.update,
label: appLocalizations.update,
iconData: Icons.sync,
),
CommonPopupMenuItem(
action: ProfileActions.delete,
label: appLocalizations.delete,
iconData: Icons.delete,
),
],
onSelected: (ProfileActions? action) async {
switch (action) {
case ProfileActions.edit:
_handleShowEditExtendPage(profile);
break;
case ProfileActions.delete:
_handleDeleteProfile(profile.id);
break;
case ProfileActions.update:
_handleUpdateProfile(profile.id);
break;
case null:
break;
}
},
),
title: _buildTitle(profile),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -50,6 +51,9 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
selectedValue: proxiesSortType,
);
},
),
const SizedBox(
width: 8,
)
];
});
@@ -57,72 +61,69 @@ class _ProxiesFragmentState extends State<ProxiesFragment>
@override
Widget build(BuildContext context) {
return DelayTestButtonContainer(
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
);
},
child: Selector3<AppState, Config, ClashConfig, ProxiesSelectorState>(
selector: (_, appState, config, clashConfig) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
);
},
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
_tabController?.dispose();
_tabController = null;
}
return prev != next;
},
builder: (_, state, __) {
_tabController ??= TabController(
length: state.groupNames.length,
vsync: this,
);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
shouldRebuild: (prev, next) {
if (prev.groupNames.length != next.groupNames.length) {
_tabController?.dispose();
_tabController = null;
}
return prev != next;
},
builder: (_, state, __) {
_tabController ??= TabController(
length: state.groupNames.length,
vsync: this,
);
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,
padding: const EdgeInsets.symmetric(horizontal: 16),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
children: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
KeepContainer(
key: ObjectKey(groupName),
child: ProxiesTabView(
groupName: groupName,
),
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxiesTabView(
groupName: groupName,
),
),
],
),
)
],
);
},
),
)
],
);
},
),
);
}
@@ -196,6 +197,23 @@ class ProxiesTabView extends StatelessWidget {
}
}
_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>(
@@ -213,25 +231,32 @@ class ProxiesTabView extends StatelessWidget {
state.group.all,
state.proxiesSortType,
);
return 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),
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,
);
},
),
itemCount: proxies.length,
itemBuilder: (_, index) {
final proxy = proxies[index];
return ProxyCard(
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
),
);
},
@@ -384,10 +409,12 @@ class ProxyCard extends StatelessWidget {
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
@@ -399,15 +426,11 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
late Animation<double> _opacity;
_healthcheck() async {
if (globalState.healthcheckLock) return;
_controller.forward();
globalState.appController.healthcheck();
Future.delayed(httpTimeoutDuration + moreDuration, () {
_controller.reverse();
});
await widget.onClick();
_controller.reverse();
}
@override
@@ -416,7 +439,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 300,
milliseconds: 200,
),
);
_scale = Tween<double>(
@@ -428,20 +451,6 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
curve: const Interval(
0,
1,
curve: Curves.elasticInOut,
),
),
);
_opacity = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
curve: Curves.easeIn,
),
),
);
@@ -455,6 +464,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
@@ -465,10 +475,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
height: 56,
child: Transform.scale(
scale: _scale.value,
child: Opacity(
opacity: _opacity.value,
child: child!,
),
child: child,
),
);
},

View File

@@ -0,0 +1,172 @@
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.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;
@immutable
class GeoItem {
final String label;
final String fileName;
const GeoItem({
required this.label,
required this.fileName,
});
}
class Resources extends StatefulWidget {
const Resources({super.key});
@override
State<Resources> createState() => _ResourcesState();
}
class _ResourcesState extends State<Resources> {
_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;
});
setState(() {});
}
Future<DateTime> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
return await File(join(homePath, fileName)).lastModified();
}
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: 2,
width: 4,
indent: 2,
),
),
externalProvider.vehicleType == "HTTP"
? IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
externalProvider.name,
externalProvider.type,
);
},
)
: Container(),
],
),
)
],
),
),
),
);
},
);
}
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,
);
},
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
_buildGeoDataSection(),
_buildExternalProviderSection(),
],
);
}
}

View File

@@ -166,6 +166,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
extendPageWidth: 360,
),
),
ListItem.open(

View File

@@ -9,6 +9,8 @@
"tools": "Tools",
"logs": "Logs",
"logsDesc": "Log capture records",
"resources": "Resources",
"resourcesDesc": "External resource related info",
"trafficUsage": "Traffic usage",
"coreInfo": "Core info",
"nullCoreInfoDesc": "Unable to obtain core info",
@@ -109,6 +111,7 @@
"noMoreInfoDesc": "No more info",
"profileParseErrorDesc": "profile parse error",
"proxyPort": "ProxyPort",
"proxyPortDesc": "Set the clash listening port",
"port": "Port",
"logLevel": "LogLevel",
"show": "Show",
@@ -151,5 +154,27 @@
"checkUpdate": "Check for updates",
"discoverNewVersion": "Discover the new version",
"checkUpdateError": "The current application is already the latest version",
"goDownload": "Go to download"
"goDownload": "Go to download",
"unknown": "Unknown",
"geoData": "GeoData",
"externalResources": "External resources",
"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",
"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",
"geodataLoader": "Geo Low Memory Mode",
"geodataLoaderDesc": "Enabling will use the Geo low memory loader"
}

View File

@@ -9,6 +9,8 @@
"tools": "工具",
"logs": "日志",
"logsDesc": "日志捕获记录",
"resources": "资源",
"resourcesDesc": "外部资源相关信息",
"trafficUsage": "流量统计",
"coreInfo": "内核信息",
"nullCoreInfoDesc": "无法获取内核信息",
@@ -109,6 +111,7 @@
"noMoreInfoDesc": "暂无更多信息",
"profileParseErrorDesc": "配置文件解析错误",
"proxyPort": "代理端口",
"proxyPortDesc": "设置clash监听端口",
"port": "端口",
"logLevel": "日志等级",
"show": "显示",
@@ -151,5 +154,27 @@
"checkUpdate": "检查更新",
"discoverNewVersion": "发现新版本",
"checkUpdateError": "当前应用已经是最新版了",
"goDownload": "前往下载"
"goDownload": "前往下载",
"unknown": "未知",
"geoData": "地理数据",
"externalResources": "外部资源",
"checking": "检测中...",
"country": "区域",
"checkError": "检测失败",
"ipCheckTimeout": "Ip检测超时",
"search": "搜索",
"allowBypass": "允许应用绕过vpn",
"allowBypassDesc": "开启后部分应用可绕过VPN",
"externalController": "外部控制器",
"externalControllerDesc": "开启后将可以通过9090端口控制clash内核",
"ipv6Desc": "开启后将可以接收ipv6流量",
"app": "应用",
"general": "基础",
"systemProxyDesc": "为VpnService附加HTTP代理",
"unifiedDelay": "统一延迟",
"unifiedDelayDesc": "去除握手等额外延迟",
"tcpConcurrent": "TCP并发",
"tcpConcurrentDesc": "开启后允许tcp并发",
"geodataLoader": "Geo低内存模式",
"geodataLoaderDesc": "开启将使用Geo低内存加载器"
}

View File

@@ -40,9 +40,14 @@ class MessageLookup extends MessageLookupByLibrary {
"addressTip": MessageLookupByLibrary.simpleMessage(
"Please enter a valid WebDAV address"),
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
"allowBypass": MessageLookupByLibrary.simpleMessage(
"Allow applications to bypass VPN"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
"Some apps can bypass VPN when turned on"),
"allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
"Allow access proxy through the LAN"),
"app": MessageLookupByLibrary.simpleMessage("App"),
"appAccessControl":
MessageLookupByLibrary.simpleMessage("App access control"),
"application": MessageLookupByLibrary.simpleMessage("Application"),
@@ -76,10 +81,12 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
"cancelSelectAll":
MessageLookupByLibrary.simpleMessage("Cancel select all"),
"checkError": MessageLookupByLibrary.simpleMessage("Check error"),
"checkUpdate":
MessageLookupByLibrary.simpleMessage("Check for updates"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
@@ -88,6 +95,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
"create": MessageLookupByLibrary.simpleMessage("Create"),
"dark": MessageLookupByLibrary.simpleMessage("Dark"),
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
@@ -109,16 +117,32 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"externalController":
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Once enabled, the clash kernel can be controlled on port 9090"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
"fileDesc":
MessageLookupByLibrary.simpleMessage("Directly upload profile"),
"filterSystemApp":
MessageLookupByLibrary.simpleMessage("Filter system app"),
"general": MessageLookupByLibrary.simpleMessage("General"),
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
"geodataLoader":
MessageLookupByLibrary.simpleMessage("Geo Low Memory Mode"),
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
"Enabling will use the Geo low memory loader"),
"global": MessageLookupByLibrary.simpleMessage("Global"),
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL":
MessageLookupByLibrary.simpleMessage("Import from URL"),
"ipCheckTimeout":
MessageLookupByLibrary.simpleMessage("Ip check timeout"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"When turned on it will be able to receive ipv6 traffic"),
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
@@ -187,6 +211,8 @@ class MessageLookup extends MessageLookupByLibrary {
"project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the clash listening port"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Scan QR code to obtain profile"),
@@ -199,8 +225,12 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
"recoverySuccess":
MessageLookupByLibrary.simpleMessage("Recovery success"),
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
"External resource related info"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
@@ -212,9 +242,14 @@ class MessageLookup extends MessageLookupByLibrary {
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"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"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage(
"Enabling it will allow tcp concurrency"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage(
@@ -229,6 +264,10 @@ class MessageLookup extends MessageLookupByLibrary {
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage(
"unable to update current profile"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("Unified delay"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage(
"Remove extra delays such as handshaking"),
"unknown": MessageLookupByLibrary.simpleMessage("Unknown"),
"update": MessageLookupByLibrary.simpleMessage("Update"),
"upload": MessageLookupByLibrary.simpleMessage("Upload"),
"url": MessageLookupByLibrary.simpleMessage("URL"),

View File

@@ -36,8 +36,12 @@ class MessageLookup extends MessageLookupByLibrary {
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"),
"allowBypassDesc":
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
@@ -63,8 +67,10 @@ class MessageLookup extends MessageLookupByLibrary {
"cancelFilterSystemApp":
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc":
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
@@ -72,6 +78,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"),
"create": MessageLookupByLibrary.simpleMessage("创建"),
"dark": MessageLookupByLibrary.simpleMessage("深色"),
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
@@ -90,13 +97,24 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制clash内核"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"general": MessageLookupByLibrary.simpleMessage("基础"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"geodataLoaderDesc":
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"global": MessageLookupByLibrary.simpleMessage("全局"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收ipv6流量"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
@@ -154,6 +172,7 @@ class MessageLookup extends MessageLookupByLibrary {
"project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置clash监听端口"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
@@ -161,8 +180,11 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("搜索"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),
@@ -173,9 +195,13 @@ class MessageLookup extends MessageLookupByLibrary {
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc":
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许tcp并发"),
"theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
@@ -187,6 +213,9 @@ class MessageLookup extends MessageLookupByLibrary {
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"),

View File

@@ -150,6 +150,26 @@ class AppLocalizations {
);
}
/// `Resources`
String get resources {
return Intl.message(
'Resources',
name: 'resources',
desc: '',
args: [],
);
}
/// `External resource related info`
String get resourcesDesc {
return Intl.message(
'External resource related info',
name: 'resourcesDesc',
desc: '',
args: [],
);
}
/// `Traffic usage`
String get trafficUsage {
return Intl.message(
@@ -1150,6 +1170,16 @@ class AppLocalizations {
);
}
/// `Set the clash listening port`
String get proxyPortDesc {
return Intl.message(
'Set the clash listening port',
name: 'proxyPortDesc',
desc: '',
args: [],
);
}
/// `Port`
String get port {
return Intl.message(
@@ -1579,6 +1609,226 @@ class AppLocalizations {
args: [],
);
}
/// `Unknown`
String get unknown {
return Intl.message(
'Unknown',
name: 'unknown',
desc: '',
args: [],
);
}
/// `GeoData`
String get geoData {
return Intl.message(
'GeoData',
name: 'geoData',
desc: '',
args: [],
);
}
/// `External resources`
String get externalResources {
return Intl.message(
'External resources',
name: 'externalResources',
desc: '',
args: [],
);
}
/// `Checking...`
String get checking {
return Intl.message(
'Checking...',
name: 'checking',
desc: '',
args: [],
);
}
/// `Country`
String get country {
return Intl.message(
'Country',
name: 'country',
desc: '',
args: [],
);
}
/// `Check error`
String get checkError {
return Intl.message(
'Check error',
name: 'checkError',
desc: '',
args: [],
);
}
/// `Ip check timeout`
String get ipCheckTimeout {
return Intl.message(
'Ip check timeout',
name: 'ipCheckTimeout',
desc: '',
args: [],
);
}
/// `Search`
String get search {
return Intl.message(
'Search',
name: 'search',
desc: '',
args: [],
);
}
/// `Allow applications to bypass VPN`
String get allowBypass {
return Intl.message(
'Allow applications to bypass VPN',
name: 'allowBypass',
desc: '',
args: [],
);
}
/// `Some apps can bypass VPN when turned on`
String get allowBypassDesc {
return Intl.message(
'Some apps can bypass VPN when turned on',
name: 'allowBypassDesc',
desc: '',
args: [],
);
}
/// `ExternalController`
String get externalController {
return Intl.message(
'ExternalController',
name: 'externalController',
desc: '',
args: [],
);
}
/// `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',
name: 'externalControllerDesc',
desc: '',
args: [],
);
}
/// `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',
name: 'ipv6Desc',
desc: '',
args: [],
);
}
/// `App`
String get app {
return Intl.message(
'App',
name: 'app',
desc: '',
args: [],
);
}
/// `General`
String get general {
return Intl.message(
'General',
name: 'general',
desc: '',
args: [],
);
}
/// `Attach HTTP proxy to VpnService`
String get systemProxyDesc {
return Intl.message(
'Attach HTTP proxy to VpnService',
name: 'systemProxyDesc',
desc: '',
args: [],
);
}
/// `Unified delay`
String get unifiedDelay {
return Intl.message(
'Unified delay',
name: 'unifiedDelay',
desc: '',
args: [],
);
}
/// `Remove extra delays such as handshaking`
String get unifiedDelayDesc {
return Intl.message(
'Remove extra delays such as handshaking',
name: 'unifiedDelayDesc',
desc: '',
args: [],
);
}
/// `Tcp concurrent`
String get tcpConcurrent {
return Intl.message(
'Tcp concurrent',
name: 'tcpConcurrent',
desc: '',
args: [],
);
}
/// `Enabling it will allow tcp concurrency`
String get tcpConcurrentDesc {
return Intl.message(
'Enabling it will allow tcp concurrency',
name: 'tcpConcurrentDesc',
desc: '',
args: [],
);
}
/// `Geo Low Memory Mode`
String get geodataLoader {
return Intl.message(
'Geo Low Memory Mode',
name: 'geodataLoader',
desc: '',
args: [],
);
}
/// `Enabling will use the Geo low memory loader`
String get geodataLoaderDesc {
return Intl.message(
'Enabling will use the Geo low memory loader',
name: 'geodataLoaderDesc',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -21,18 +21,17 @@ Future<void> main() async {
mode: clashConfig.mode,
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
viewWidth: other.getViewWidth(),
);
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: false,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
globalState.updateNavigationItems(
appState: appState,
config: config,
clashConfig: clashConfig,
);
runAppWithPreferences(
const Application(),
appState: appState,
@@ -62,6 +61,16 @@ Future<void> vpnService() async {
clashConfig: clashConfig,
);
if (appState.isInit) {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
} else {
exit(0);
}
final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,

View File

@@ -32,7 +32,6 @@ class AppState with ChangeNotifier {
AppState({
required Mode mode,
double? viewWidth,
required bool isCompatible,
required SelectedMap selectedMap,
}) : _navigationItems = [],
@@ -40,7 +39,7 @@ class AppState with ChangeNotifier {
_currentLabel = "dashboard",
_traffics = [],
_logs = [],
_viewWidth = viewWidth ?? 0,
_viewWidth = 0,
_selectedMap = selectedMap,
_sortNum = 0,
_mode = mode,
@@ -90,6 +89,8 @@ class AppState with ChangeNotifier {
}
}
bool get isStart => _runTime != null;
int? get runTime => _runTime;
set runTime(int? value) {
@@ -106,7 +107,7 @@ class AppState with ChangeNotifier {
} else {
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return type;
return "$type(${groups[index].now})";
return "$type(${groups[index].now ?? '*'})";
}
}
@@ -167,6 +168,9 @@ class AppState with ChangeNotifier {
addLog(Log log) {
_logs.add(log);
if (_logs.length > 60) {
_logs = _logs.sublist(_logs.length - 60);
}
notifyListeners();
}
@@ -179,7 +183,6 @@ class AppState with ChangeNotifier {
}
}
List<Group> get groups => _groups;
set groups(List<Group> value) {
@@ -253,11 +256,7 @@ class AppState with ChangeNotifier {
}
}
ViewMode get viewMode {
if (_viewWidth <= maxMobileWidth) return ViewMode.mobile;
if (_viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop;
}
ViewMode get viewMode => other.getViewMode(_viewWidth);
DelayMap get delayMap {
return _delayMap;

View File

@@ -108,8 +108,13 @@ class Dns {
class ClashConfig extends ChangeNotifier {
int _mixedPort;
bool _allowLan;
Mode _mode;
bool _ipv6;
String _geodataLoader;
LogLevel _logLevel;
String _externalController;
Mode _mode;
bool _unifiedDelay;
bool _tcpConcurrent;
Tun _tun;
Dns _dns;
List<String> _rules;
@@ -118,15 +123,25 @@ class ClashConfig extends ChangeNotifier {
int? mixedPort,
Mode? mode,
bool? allowLan,
bool? ipv6,
LogLevel? logLevel,
String? externalController,
String? geodataLoader,
bool? unifiedDelay,
Tun? tun,
Dns? dns,
bool? tcpConcurrent,
List<String>? rules,
}) : _mixedPort = mixedPort ?? 7890,
_mode = mode ?? Mode.rule,
_ipv6 = ipv6 ?? false,
_allowLan = allowLan ?? false,
_tcpConcurrent = tcpConcurrent ?? false,
_logLevel = logLevel ?? LogLevel.info,
_tun = tun ?? const Tun(),
_unifiedDelay = unifiedDelay ?? false,
_geodataLoader = geodataLoader ?? geodataLoaderMemconservative,
_externalController = externalController ?? '',
_dns = dns ?? Dns(),
_rules = rules ?? [];
@@ -169,6 +184,56 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "external-controller", defaultValue: '')
String get externalController => _externalController;
set externalController(String value) {
if (_externalController != value) {
_externalController = value;
notifyListeners();
}
}
@JsonKey(defaultValue: false)
bool get ipv6 => _ipv6;
set ipv6(bool value) {
if (_ipv6 != value) {
_ipv6 = value;
notifyListeners();
}
}
@JsonKey(name: "geodata-loader", defaultValue: geodataLoaderMemconservative)
String get geodataLoader => _geodataLoader;
set geodataLoader(String value) {
if (_geodataLoader != value) {
_geodataLoader = value;
notifyListeners();
}
}
@JsonKey(name: "unified-delay", defaultValue: false)
bool get unifiedDelay => _unifiedDelay;
set unifiedDelay(bool value) {
if (_unifiedDelay != value) {
_unifiedDelay = value;
notifyListeners();
}
}
@JsonKey(name: "tcp-concurrent", defaultValue: false)
bool get tcpConcurrent => _tcpConcurrent;
set tcpConcurrent(bool value) {
if (_tcpConcurrent != value) {
_tcpConcurrent = value;
notifyListeners();
}
}
Tun get tun => _tun;
set tun(Tun value) {

View File

@@ -1,24 +0,0 @@
import 'package:fl_clash/enum/enum.dart';
class Result<T> {
String? message;
ResultType type;
T? data;
Result({
this.message,
required this.type,
this.data,
});
Result.success([this.data]) : type = ResultType.success,
message = null;
Result.error([this.message]) : type = ResultType.error,
data = null;
@override
String toString() {
return 'Result{message: $message, type: $type, data: $data}';
}
}

View File

@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../enum/enum.dart';
import '../common/common.dart';
@@ -9,70 +9,31 @@ import 'models.dart';
part 'generated/config.g.dart';
@JsonSerializable()
class AccessControl {
AccessControlMode mode;
List<String> acceptList;
List<String> rejectList;
bool isFilterSystemApp;
part 'generated/config.freezed.dart';
AccessControl({
this.isFilterSystemApp = true,
this.mode = AccessControlMode.rejectSelected,
this.acceptList = const [],
this.rejectList = const [],
});
@freezed
class AccessControl with _$AccessControl {
const factory AccessControl({
@Default(AccessControlMode.rejectSelected) AccessControlMode mode,
@Default([]) List<String> acceptList,
@Default([]) List<String> rejectList,
@Default(true) bool isFilterSystemApp,
}) = _AccessControl;
@JsonKey(includeFromJson: false, includeToJson: false)
List<String> get currentList =>
mode == AccessControlMode.acceptSelected ? acceptList : rejectList;
factory AccessControl.fromJson(Map<String, Object?> json) =>
_$AccessControlFromJson(json);
}
set currentList(List<String> currentList) {
if (mode == AccessControlMode.acceptSelected) {
acceptList = currentList;
} else {
rejectList = currentList;
}
}
@freezed
class Props with _$Props {
const factory Props({
AccessControl? accessControl,
bool? allowBypass,
bool? systemProxy,
}) = _Props;
AccessControl copyWith({
AccessControlMode? mode,
List<String>? acceptList,
List<String>? rejectList,
bool? isFilterSystemApp,
}) {
return AccessControl(
mode: mode ?? this.mode,
acceptList: acceptList ?? this.acceptList,
rejectList: rejectList ?? this.rejectList,
isFilterSystemApp: isFilterSystemApp ?? this.isFilterSystemApp,
);
}
Map<String, dynamic> toJson() {
return _$AccessControlToJson(this);
}
factory AccessControl.fromJson(Map<String, dynamic> json) {
return _$AccessControlFromJson(json);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AccessControl &&
runtimeType == other.runtimeType &&
mode == other.mode &&
acceptList == other.acceptList &&
rejectList == other.rejectList &&
isFilterSystemApp == other.isFilterSystemApp;
@override
int get hashCode =>
mode.hashCode ^
acceptList.hashCode ^
rejectList.hashCode ^
isFilterSystemApp.hashCode;
factory Props.fromJson(Map<String, Object?> json) =>
_$PropsFromJson(json);
}
@JsonSerializable()
@@ -93,6 +54,8 @@ class Config extends ChangeNotifier {
AccessControl _accessControl;
bool _isAnimateToPage;
bool _autoCheckUpdate;
bool _allowBypass;
bool _systemProxy;
DAV? _dav;
Config()
@@ -102,14 +65,16 @@ class Config extends ChangeNotifier {
_autoRun = false,
_themeMode = ThemeMode.system,
_openLog = false,
_isCompatible = false,
_isCompatible = true,
_primaryColor = defaultPrimaryColor.value,
_proxiesSortType = ProxiesSortType.none,
_isMinimizeOnExit = true,
_isAccessControl = false,
_autoCheckUpdate = true,
_accessControl = AccessControl(),
_isAnimateToPage = true;
_systemProxy = false,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
_allowBypass = true;
deleteProfileById(String id) {
_profiles = profiles.where((element) => element.id != id).toList();
@@ -333,7 +298,7 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: false)
@JsonKey(defaultValue: true)
bool get isCompatible {
return _isCompatible;
}
@@ -357,17 +322,44 @@ class Config extends ChangeNotifier {
}
}
update([Config? config, RecoveryOption recoveryOptions = RecoveryOption.all]) {
@JsonKey(defaultValue: true)
bool get allowBypass {
return _allowBypass;
}
set allowBypass(bool value) {
if (_allowBypass != value) {
_allowBypass = value;
notifyListeners();
}
}
@JsonKey(defaultValue: true)
bool get systemProxy {
return _systemProxy;
}
set systemProxy(bool value) {
if (_systemProxy != value) {
_systemProxy = value;
notifyListeners();
}
}
update([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
]) {
if (config != null) {
_profiles = config._profiles;
for (final profile in config._profiles) {
_setProfile(profile);
}
final onlyProfiles = recoveryOptions == RecoveryOption.onlyProfiles;
if(_currentProfileId == null && onlyProfiles && profiles.isNotEmpty){
if (_currentProfileId == null && onlyProfiles && profiles.isNotEmpty) {
_currentProfileId = _profiles.first.id;
}
if(onlyProfiles) return;
if (onlyProfiles) return;
_currentProfileId = config._currentProfileId;
_isCompatible = config._isCompatible;
_autoLaunch = config._autoLaunch;
@@ -376,6 +368,7 @@ class Config extends ChangeNotifier {
_openLog = config._openLog;
_themeMode = config._themeMode;
_locale = config._locale;
_allowBypass = config._allowBypass;
_primaryColor = config._primaryColor;
_proxiesSortType = config._proxiesSortType;
_isMinimizeOnExit = config._isMinimizeOnExit;

View File

@@ -75,3 +75,16 @@ class Process with _$Process {
factory Process.fromJson(Map<String, Object?> json) =>
_$ProcessFromJson(json);
}
@freezed
class ExternalProvider with _$ExternalProvider {
const factory ExternalProvider({
required String name,
required String type,
@JsonKey(name: "vehicle-type") required String vehicleType,
@JsonKey(name: "update-at") required DateTime updateAt,
}) = _ExternalProvider;
factory ExternalProvider.fromJson(Map<String, Object?> json) =>
_$ExternalProviderFromJson(json);
}

View File

@@ -39,13 +39,18 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig(
mixedPort: (json['mixed-port'] as num?)?.toInt(),
mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']),
allowLan: json['allow-lan'] as bool?,
ipv6: json['ipv6'] as bool? ?? false,
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']),
externalController: json['external-controller'] as String? ?? '',
geodataLoader: json['geodata-loader'] as String? ?? 'memconservative',
unifiedDelay: json['unified-delay'] as bool? ?? false,
tun: json['tun'] == null
? null
: Tun.fromJson(json['tun'] as Map<String, dynamic>),
dns: json['dns'] == null
? null
: Dns.fromJson(json['dns'] as Map<String, dynamic>),
tcpConcurrent: json['tcp-concurrent'] as bool? ?? false,
rules:
(json['rules'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
@@ -56,6 +61,11 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'mode': _$ModeEnumMap[instance.mode]!,
'allow-lan': instance.allowLan,
'log-level': _$LogLevelEnumMap[instance.logLevel]!,
'external-controller': instance.externalController,
'ipv6': instance.ipv6,
'geodata-loader': instance.geodataLoader,
'unified-delay': instance.unifiedDelay,
'tcp-concurrent': instance.tcpConcurrent,
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,

View File

@@ -0,0 +1,431 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of '../config.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
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');
AccessControl _$AccessControlFromJson(Map<String, dynamic> json) {
return _AccessControl.fromJson(json);
}
/// @nodoc
mixin _$AccessControl {
AccessControlMode get mode => throw _privateConstructorUsedError;
List<String> get acceptList => throw _privateConstructorUsedError;
List<String> get rejectList => throw _privateConstructorUsedError;
bool get isFilterSystemApp => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AccessControlCopyWith<AccessControl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AccessControlCopyWith<$Res> {
factory $AccessControlCopyWith(
AccessControl value, $Res Function(AccessControl) then) =
_$AccessControlCopyWithImpl<$Res, AccessControl>;
@useResult
$Res call(
{AccessControlMode mode,
List<String> acceptList,
List<String> rejectList,
bool isFilterSystemApp});
}
/// @nodoc
class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl>
implements $AccessControlCopyWith<$Res> {
_$AccessControlCopyWithImpl(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? mode = null,
Object? acceptList = null,
Object? rejectList = null,
Object? isFilterSystemApp = null,
}) {
return _then(_value.copyWith(
mode: null == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as AccessControlMode,
acceptList: null == acceptList
? _value.acceptList
: acceptList // ignore: cast_nullable_to_non_nullable
as List<String>,
rejectList: null == rejectList
? _value.rejectList
: rejectList // ignore: cast_nullable_to_non_nullable
as List<String>,
isFilterSystemApp: null == isFilterSystemApp
? _value.isFilterSystemApp
: isFilterSystemApp // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$AccessControlImplCopyWith<$Res>
implements $AccessControlCopyWith<$Res> {
factory _$$AccessControlImplCopyWith(
_$AccessControlImpl value, $Res Function(_$AccessControlImpl) then) =
__$$AccessControlImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AccessControlMode mode,
List<String> acceptList,
List<String> rejectList,
bool isFilterSystemApp});
}
/// @nodoc
class __$$AccessControlImplCopyWithImpl<$Res>
extends _$AccessControlCopyWithImpl<$Res, _$AccessControlImpl>
implements _$$AccessControlImplCopyWith<$Res> {
__$$AccessControlImplCopyWithImpl(
_$AccessControlImpl _value, $Res Function(_$AccessControlImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? mode = null,
Object? acceptList = null,
Object? rejectList = null,
Object? isFilterSystemApp = null,
}) {
return _then(_$AccessControlImpl(
mode: null == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as AccessControlMode,
acceptList: null == acceptList
? _value._acceptList
: acceptList // ignore: cast_nullable_to_non_nullable
as List<String>,
rejectList: null == rejectList
? _value._rejectList
: rejectList // ignore: cast_nullable_to_non_nullable
as List<String>,
isFilterSystemApp: null == isFilterSystemApp
? _value.isFilterSystemApp
: isFilterSystemApp // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AccessControlImpl implements _AccessControl {
const _$AccessControlImpl(
{this.mode = AccessControlMode.rejectSelected,
final List<String> acceptList = const [],
final List<String> rejectList = const [],
this.isFilterSystemApp = true})
: _acceptList = acceptList,
_rejectList = rejectList;
factory _$AccessControlImpl.fromJson(Map<String, dynamic> json) =>
_$$AccessControlImplFromJson(json);
@override
@JsonKey()
final AccessControlMode mode;
final List<String> _acceptList;
@override
@JsonKey()
List<String> get acceptList {
if (_acceptList is EqualUnmodifiableListView) return _acceptList;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_acceptList);
}
final List<String> _rejectList;
@override
@JsonKey()
List<String> get rejectList {
if (_rejectList is EqualUnmodifiableListView) return _rejectList;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_rejectList);
}
@override
@JsonKey()
final bool isFilterSystemApp;
@override
String toString() {
return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, isFilterSystemApp: $isFilterSystemApp)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AccessControlImpl &&
(identical(other.mode, mode) || other.mode == mode) &&
const DeepCollectionEquality()
.equals(other._acceptList, _acceptList) &&
const DeepCollectionEquality()
.equals(other._rejectList, _rejectList) &&
(identical(other.isFilterSystemApp, isFilterSystemApp) ||
other.isFilterSystemApp == isFilterSystemApp));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
mode,
const DeepCollectionEquality().hash(_acceptList),
const DeepCollectionEquality().hash(_rejectList),
isFilterSystemApp);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith =>
__$$AccessControlImplCopyWithImpl<_$AccessControlImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AccessControlImplToJson(
this,
);
}
}
abstract class _AccessControl implements AccessControl {
const factory _AccessControl(
{final AccessControlMode mode,
final List<String> acceptList,
final List<String> rejectList,
final bool isFilterSystemApp}) = _$AccessControlImpl;
factory _AccessControl.fromJson(Map<String, dynamic> json) =
_$AccessControlImpl.fromJson;
@override
AccessControlMode get mode;
@override
List<String> get acceptList;
@override
List<String> get rejectList;
@override
bool get isFilterSystemApp;
@override
@JsonKey(ignore: true)
_$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Props _$PropsFromJson(Map<String, dynamic> json) {
return _Props.fromJson(json);
}
/// @nodoc
mixin _$Props {
AccessControl? get accessControl => throw _privateConstructorUsedError;
bool? get allowBypass => throw _privateConstructorUsedError;
bool? get systemProxy => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$PropsCopyWith<Props> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PropsCopyWith<$Res> {
factory $PropsCopyWith(Props value, $Res Function(Props) then) =
_$PropsCopyWithImpl<$Res, Props>;
@useResult
$Res call(
{AccessControl? accessControl, bool? allowBypass, bool? systemProxy});
$AccessControlCopyWith<$Res>? get accessControl;
}
/// @nodoc
class _$PropsCopyWithImpl<$Res, $Val extends Props>
implements $PropsCopyWith<$Res> {
_$PropsCopyWithImpl(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? accessControl = freezed,
Object? allowBypass = freezed,
Object? systemProxy = freezed,
}) {
return _then(_value.copyWith(
accessControl: freezed == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
allowBypass: freezed == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool?,
systemProxy: freezed == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AccessControlCopyWith<$Res>? get accessControl {
if (_value.accessControl == null) {
return null;
}
return $AccessControlCopyWith<$Res>(_value.accessControl!, (value) {
return _then(_value.copyWith(accessControl: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> {
factory _$$PropsImplCopyWith(
_$PropsImpl value, $Res Function(_$PropsImpl) then) =
__$$PropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AccessControl? accessControl, bool? allowBypass, bool? systemProxy});
@override
$AccessControlCopyWith<$Res>? get accessControl;
}
/// @nodoc
class __$$PropsImplCopyWithImpl<$Res>
extends _$PropsCopyWithImpl<$Res, _$PropsImpl>
implements _$$PropsImplCopyWith<$Res> {
__$$PropsImplCopyWithImpl(
_$PropsImpl _value, $Res Function(_$PropsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accessControl = freezed,
Object? allowBypass = freezed,
Object? systemProxy = freezed,
}) {
return _then(_$PropsImpl(
accessControl: freezed == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
allowBypass: freezed == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool?,
systemProxy: freezed == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$PropsImpl implements _Props {
const _$PropsImpl({this.accessControl, this.allowBypass, this.systemProxy});
factory _$PropsImpl.fromJson(Map<String, dynamic> json) =>
_$$PropsImplFromJson(json);
@override
final AccessControl? accessControl;
@override
final bool? allowBypass;
@override
final bool? systemProxy;
@override
String toString() {
return 'Props(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PropsImpl &&
(identical(other.accessControl, accessControl) ||
other.accessControl == accessControl) &&
(identical(other.allowBypass, allowBypass) ||
other.allowBypass == allowBypass) &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, accessControl, allowBypass, systemProxy);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
__$$PropsImplCopyWithImpl<_$PropsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$PropsImplToJson(
this,
);
}
}
abstract class _Props implements Props {
const factory _Props(
{final AccessControl? accessControl,
final bool? allowBypass,
final bool? systemProxy}) = _$PropsImpl;
factory _Props.fromJson(Map<String, dynamic> json) = _$PropsImpl.fromJson;
@override
AccessControl? get accessControl;
@override
bool? get allowBypass;
@override
bool? get systemProxy;
@override
@JsonKey(ignore: true)
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -6,34 +6,6 @@ part of '../config.dart';
// JsonSerializableGenerator
// **************************************************************************
AccessControl _$AccessControlFromJson(Map<String, dynamic> json) =>
AccessControl(
isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true,
mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ??
AccessControlMode.rejectSelected,
acceptList: (json['acceptList'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
rejectList: (json['rejectList'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$AccessControlToJson(AccessControl instance) =>
<String, dynamic>{
'mode': _$AccessControlModeEnumMap[instance.mode]!,
'acceptList': instance.acceptList,
'rejectList': instance.rejectList,
'isFilterSystemApp': instance.isFilterSystemApp,
};
const _$AccessControlModeEnumMap = {
AccessControlMode.acceptSelected: 'acceptSelected',
AccessControlMode.rejectSelected: 'rejectSelected',
};
Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..profiles = (json['profiles'] as List<dynamic>?)
?.map((e) => Profile.fromJson(e as Map<String, dynamic>))
@@ -59,8 +31,10 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
? null
: DAV.fromJson(json['dav'] as Map<String, dynamic>)
..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true
..isCompatible = json['isCompatible'] as bool? ?? false
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true;
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true;
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -80,6 +54,8 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'isAnimateToPage': instance.isAnimateToPage,
'isCompatible': instance.isCompatible,
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
};
const _$ThemeModeEnumMap = {
@@ -93,3 +69,47 @@ const _$ProxiesSortTypeEnumMap = {
ProxiesSortType.delay: 'delay',
ProxiesSortType.name: 'name',
};
_$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
_$AccessControlImpl(
mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ??
AccessControlMode.rejectSelected,
acceptList: (json['acceptList'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
rejectList: (json['rejectList'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true,
);
Map<String, dynamic> _$$AccessControlImplToJson(_$AccessControlImpl instance) =>
<String, dynamic>{
'mode': _$AccessControlModeEnumMap[instance.mode]!,
'acceptList': instance.acceptList,
'rejectList': instance.rejectList,
'isFilterSystemApp': instance.isFilterSystemApp,
};
const _$AccessControlModeEnumMap = {
AccessControlMode.acceptSelected: 'acceptSelected',
AccessControlMode.rejectSelected: 'rejectSelected',
};
_$PropsImpl _$$PropsImplFromJson(Map<String, dynamic> json) => _$PropsImpl(
accessControl: json['accessControl'] == null
? null
: AccessControl.fromJson(
json['accessControl'] as Map<String, dynamic>),
allowBypass: json['allowBypass'] as bool?,
systemProxy: json['systemProxy'] as bool?,
);
Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
<String, dynamic>{
'accessControl': instance.accessControl,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
};

View File

@@ -1029,3 +1029,214 @@ abstract class _Process implements Process {
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ExternalProvider _$ExternalProviderFromJson(Map<String, dynamic> json) {
return _ExternalProvider.fromJson(json);
}
/// @nodoc
mixin _$ExternalProvider {
String get name => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
@JsonKey(name: "vehicle-type")
String get vehicleType => throw _privateConstructorUsedError;
@JsonKey(name: "update-at")
DateTime get updateAt => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ExternalProviderCopyWith<ExternalProvider> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ExternalProviderCopyWith<$Res> {
factory $ExternalProviderCopyWith(
ExternalProvider value, $Res Function(ExternalProvider) then) =
_$ExternalProviderCopyWithImpl<$Res, ExternalProvider>;
@useResult
$Res call(
{String name,
String type,
@JsonKey(name: "vehicle-type") String vehicleType,
@JsonKey(name: "update-at") DateTime updateAt});
}
/// @nodoc
class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider>
implements $ExternalProviderCopyWith<$Res> {
_$ExternalProviderCopyWithImpl(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? name = null,
Object? type = null,
Object? vehicleType = null,
Object? updateAt = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
vehicleType: null == vehicleType
? _value.vehicleType
: vehicleType // ignore: cast_nullable_to_non_nullable
as String,
updateAt: null == updateAt
? _value.updateAt
: updateAt // ignore: cast_nullable_to_non_nullable
as DateTime,
) as $Val);
}
}
/// @nodoc
abstract class _$$ExternalProviderImplCopyWith<$Res>
implements $ExternalProviderCopyWith<$Res> {
factory _$$ExternalProviderImplCopyWith(_$ExternalProviderImpl value,
$Res Function(_$ExternalProviderImpl) then) =
__$$ExternalProviderImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String name,
String type,
@JsonKey(name: "vehicle-type") String vehicleType,
@JsonKey(name: "update-at") DateTime updateAt});
}
/// @nodoc
class __$$ExternalProviderImplCopyWithImpl<$Res>
extends _$ExternalProviderCopyWithImpl<$Res, _$ExternalProviderImpl>
implements _$$ExternalProviderImplCopyWith<$Res> {
__$$ExternalProviderImplCopyWithImpl(_$ExternalProviderImpl _value,
$Res Function(_$ExternalProviderImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? type = null,
Object? vehicleType = null,
Object? updateAt = null,
}) {
return _then(_$ExternalProviderImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
vehicleType: null == vehicleType
? _value.vehicleType
: vehicleType // ignore: cast_nullable_to_non_nullable
as String,
updateAt: null == updateAt
? _value.updateAt
: updateAt // ignore: cast_nullable_to_non_nullable
as DateTime,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ExternalProviderImpl implements _ExternalProvider {
const _$ExternalProviderImpl(
{required this.name,
required this.type,
@JsonKey(name: "vehicle-type") required this.vehicleType,
@JsonKey(name: "update-at") required this.updateAt});
factory _$ExternalProviderImpl.fromJson(Map<String, dynamic> json) =>
_$$ExternalProviderImplFromJson(json);
@override
final String name;
@override
final String type;
@override
@JsonKey(name: "vehicle-type")
final String vehicleType;
@override
@JsonKey(name: "update-at")
final DateTime updateAt;
@override
String toString() {
return 'ExternalProvider(name: $name, type: $type, vehicleType: $vehicleType, updateAt: $updateAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ExternalProviderImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.vehicleType, vehicleType) ||
other.vehicleType == vehicleType) &&
(identical(other.updateAt, updateAt) ||
other.updateAt == updateAt));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, name, type, vehicleType, updateAt);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ExternalProviderImplCopyWith<_$ExternalProviderImpl> get copyWith =>
__$$ExternalProviderImplCopyWithImpl<_$ExternalProviderImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ExternalProviderImplToJson(
this,
);
}
}
abstract class _ExternalProvider implements ExternalProvider {
const factory _ExternalProvider(
{required final String name,
required final String type,
@JsonKey(name: "vehicle-type") required final String vehicleType,
@JsonKey(name: "update-at") required final DateTime updateAt}) =
_$ExternalProviderImpl;
factory _ExternalProvider.fromJson(Map<String, dynamic> json) =
_$ExternalProviderImpl.fromJson;
@override
String get name;
@override
String get type;
@override
@JsonKey(name: "vehicle-type")
String get vehicleType;
@override
@JsonKey(name: "update-at")
DateTime get updateAt;
@override
@JsonKey(ignore: true)
_$$ExternalProviderImplCopyWith<_$ExternalProviderImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -94,3 +94,21 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
'source': instance.source,
'target': instance.target,
};
_$ExternalProviderImpl _$$ExternalProviderImplFromJson(
Map<String, dynamic> json) =>
_$ExternalProviderImpl(
name: json['name'] as String,
type: json['type'] as String,
vehicleType: json['vehicle-type'] as String,
updateAt: DateTime.parse(json['update-at'] as String),
);
Map<String, dynamic> _$$ExternalProviderImplToJson(
_$ExternalProviderImpl instance) =>
<String, dynamic>{
'name': instance.name,
'type': instance.type,
'vehicle-type': instance.vehicleType,
'update-at': instance.updateAt.toIso8601String(),
};

View File

@@ -20,6 +20,7 @@ mixin _$NavigationItem {
String get label => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
Widget get fragment => throw _privateConstructorUsedError;
bool get keep => throw _privateConstructorUsedError;
String? get path => throw _privateConstructorUsedError;
List<NavigationItemMode> get modes => throw _privateConstructorUsedError;
@@ -39,6 +40,7 @@ abstract class $NavigationItemCopyWith<$Res> {
String label,
String? description,
Widget fragment,
bool keep,
String? path,
List<NavigationItemMode> modes});
}
@@ -60,6 +62,7 @@ class _$NavigationItemCopyWithImpl<$Res, $Val extends NavigationItem>
Object? label = null,
Object? description = freezed,
Object? fragment = null,
Object? keep = null,
Object? path = freezed,
Object? modes = null,
}) {
@@ -80,6 +83,10 @@ class _$NavigationItemCopyWithImpl<$Res, $Val extends NavigationItem>
? _value.fragment
: fragment // ignore: cast_nullable_to_non_nullable
as Widget,
keep: null == keep
? _value.keep
: keep // ignore: cast_nullable_to_non_nullable
as bool,
path: freezed == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
@@ -105,6 +112,7 @@ abstract class _$$NavigationItemImplCopyWith<$Res>
String label,
String? description,
Widget fragment,
bool keep,
String? path,
List<NavigationItemMode> modes});
}
@@ -124,6 +132,7 @@ class __$$NavigationItemImplCopyWithImpl<$Res>
Object? label = null,
Object? description = freezed,
Object? fragment = null,
Object? keep = null,
Object? path = freezed,
Object? modes = null,
}) {
@@ -144,6 +153,10 @@ class __$$NavigationItemImplCopyWithImpl<$Res>
? _value.fragment
: fragment // ignore: cast_nullable_to_non_nullable
as Widget,
keep: null == keep
? _value.keep
: keep // ignore: cast_nullable_to_non_nullable
as bool,
path: freezed == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
@@ -164,6 +177,7 @@ class _$NavigationItemImpl implements _NavigationItem {
required this.label,
this.description,
required this.fragment,
this.keep = true,
this.path,
final List<NavigationItemMode> modes = const [
NavigationItemMode.mobile,
@@ -180,6 +194,9 @@ class _$NavigationItemImpl implements _NavigationItem {
@override
final Widget fragment;
@override
@JsonKey()
final bool keep;
@override
final String? path;
final List<NavigationItemMode> _modes;
@override
@@ -192,7 +209,7 @@ class _$NavigationItemImpl implements _NavigationItem {
@override
String toString() {
return 'NavigationItem(icon: $icon, label: $label, description: $description, fragment: $fragment, path: $path, modes: $modes)';
return 'NavigationItem(icon: $icon, label: $label, description: $description, fragment: $fragment, keep: $keep, path: $path, modes: $modes)';
}
@override
@@ -206,13 +223,14 @@ class _$NavigationItemImpl implements _NavigationItem {
other.description == description) &&
(identical(other.fragment, fragment) ||
other.fragment == fragment) &&
(identical(other.keep, keep) || other.keep == keep) &&
(identical(other.path, path) || other.path == path) &&
const DeepCollectionEquality().equals(other._modes, _modes));
}
@override
int get hashCode => Object.hash(runtimeType, icon, label, description,
fragment, path, const DeepCollectionEquality().hash(_modes));
fragment, keep, path, const DeepCollectionEquality().hash(_modes));
@JsonKey(ignore: true)
@override
@@ -228,6 +246,7 @@ abstract class _NavigationItem implements NavigationItem {
required final String label,
final String? description,
required final Widget fragment,
final bool keep,
final String? path,
final List<NavigationItemMode> modes}) = _$NavigationItemImpl;
@@ -240,6 +259,8 @@ abstract class _NavigationItem implements NavigationItem {
@override
Widget get fragment;
@override
bool get keep;
@override
String? get path;
@override
List<NavigationItemMode> get modes;

View File

@@ -28,6 +28,8 @@ const _$GroupTypeEnumMap = {
GroupType.Selector: 'Selector',
GroupType.URLTest: 'URLTest',
GroupType.Fallback: 'Fallback',
GroupType.LoadBalance: 'LoadBalance',
GroupType.Relay: 'Relay',
};
_$ProxyImpl _$$ProxyImplFromJson(Map<String, dynamic> json) => _$ProxyImpl(

View File

@@ -157,34 +157,30 @@ abstract class _StartButtonSelectorState implements StartButtonSelectorState {
}
/// @nodoc
mixin _$UpdateCurrentDelaySelectorState {
String? get currentProxyName => throw _privateConstructorUsedError;
bool get isCurrent => throw _privateConstructorUsedError;
int? get delay => throw _privateConstructorUsedError;
mixin _$CheckIpSelectorState {
bool get isInit => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError;
Map<String, String> get selectedMap => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$UpdateCurrentDelaySelectorStateCopyWith<UpdateCurrentDelaySelectorState>
get copyWith => throw _privateConstructorUsedError;
$CheckIpSelectorStateCopyWith<CheckIpSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UpdateCurrentDelaySelectorStateCopyWith<$Res> {
factory $UpdateCurrentDelaySelectorStateCopyWith(
UpdateCurrentDelaySelectorState value,
$Res Function(UpdateCurrentDelaySelectorState) then) =
_$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
UpdateCurrentDelaySelectorState>;
abstract class $CheckIpSelectorStateCopyWith<$Res> {
factory $CheckIpSelectorStateCopyWith(CheckIpSelectorState value,
$Res Function(CheckIpSelectorState) then) =
_$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>;
@useResult
$Res call(
{String? currentProxyName, bool isCurrent, int? delay, bool isInit});
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
}
/// @nodoc
class _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
$Val extends UpdateCurrentDelaySelectorState>
implements $UpdateCurrentDelaySelectorStateCopyWith<$Res> {
_$UpdateCurrentDelaySelectorStateCopyWithImpl(this._value, this._then);
class _$CheckIpSelectorStateCopyWithImpl<$Res,
$Val extends CheckIpSelectorState>
implements $CheckIpSelectorStateCopyWith<$Res> {
_$CheckIpSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
@@ -194,154 +190,136 @@ class _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentProxyName = freezed,
Object? isCurrent = null,
Object? delay = freezed,
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
}) {
return _then(_value.copyWith(
currentProxyName: freezed == currentProxyName
? _value.currentProxyName
: currentProxyName // ignore: cast_nullable_to_non_nullable
as String?,
isCurrent: null == isCurrent
? _value.isCurrent
: isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
delay: freezed == delay
? _value.delay
: delay // ignore: cast_nullable_to_non_nullable
as int?,
isInit: null == isInit
? _value.isInit
: isInit // ignore: cast_nullable_to_non_nullable
as bool,
isStart: null == isStart
? _value.isStart
: isStart // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value.selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$UpdateCurrentDelaySelectorStateImplCopyWith<$Res>
implements $UpdateCurrentDelaySelectorStateCopyWith<$Res> {
factory _$$UpdateCurrentDelaySelectorStateImplCopyWith(
_$UpdateCurrentDelaySelectorStateImpl value,
$Res Function(_$UpdateCurrentDelaySelectorStateImpl) then) =
__$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<$Res>;
abstract class _$$CheckIpSelectorStateImplCopyWith<$Res>
implements $CheckIpSelectorStateCopyWith<$Res> {
factory _$$CheckIpSelectorStateImplCopyWith(_$CheckIpSelectorStateImpl value,
$Res Function(_$CheckIpSelectorStateImpl) then) =
__$$CheckIpSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String? currentProxyName, bool isCurrent, int? delay, bool isInit});
$Res call({bool isInit, bool isStart, Map<String, String> selectedMap});
}
/// @nodoc
class __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<$Res>
extends _$UpdateCurrentDelaySelectorStateCopyWithImpl<$Res,
_$UpdateCurrentDelaySelectorStateImpl>
implements _$$UpdateCurrentDelaySelectorStateImplCopyWith<$Res> {
__$$UpdateCurrentDelaySelectorStateImplCopyWithImpl(
_$UpdateCurrentDelaySelectorStateImpl _value,
$Res Function(_$UpdateCurrentDelaySelectorStateImpl) _then)
class __$$CheckIpSelectorStateImplCopyWithImpl<$Res>
extends _$CheckIpSelectorStateCopyWithImpl<$Res, _$CheckIpSelectorStateImpl>
implements _$$CheckIpSelectorStateImplCopyWith<$Res> {
__$$CheckIpSelectorStateImplCopyWithImpl(_$CheckIpSelectorStateImpl _value,
$Res Function(_$CheckIpSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentProxyName = freezed,
Object? isCurrent = null,
Object? delay = freezed,
Object? isInit = null,
Object? isStart = null,
Object? selectedMap = null,
}) {
return _then(_$UpdateCurrentDelaySelectorStateImpl(
currentProxyName: freezed == currentProxyName
? _value.currentProxyName
: currentProxyName // ignore: cast_nullable_to_non_nullable
as String?,
isCurrent: null == isCurrent
? _value.isCurrent
: isCurrent // ignore: cast_nullable_to_non_nullable
as bool,
delay: freezed == delay
? _value.delay
: delay // ignore: cast_nullable_to_non_nullable
as int?,
return _then(_$CheckIpSelectorStateImpl(
isInit: null == isInit
? _value.isInit
: isInit // ignore: cast_nullable_to_non_nullable
as bool,
isStart: null == isStart
? _value.isStart
: isStart // ignore: cast_nullable_to_non_nullable
as bool,
selectedMap: null == selectedMap
? _value._selectedMap
: selectedMap // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
));
}
}
/// @nodoc
class _$UpdateCurrentDelaySelectorStateImpl
implements _UpdateCurrentDelaySelectorState {
const _$UpdateCurrentDelaySelectorStateImpl(
{required this.currentProxyName,
required this.isCurrent,
required this.delay,
required this.isInit});
class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState {
const _$CheckIpSelectorStateImpl(
{required this.isInit,
required this.isStart,
required final Map<String, String> selectedMap})
: _selectedMap = selectedMap;
@override
final String? currentProxyName;
@override
final bool isCurrent;
@override
final int? delay;
@override
final bool isInit;
@override
final bool isStart;
final Map<String, String> _selectedMap;
@override
Map<String, String> get selectedMap {
if (_selectedMap is EqualUnmodifiableMapView) return _selectedMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_selectedMap);
}
@override
String toString() {
return 'UpdateCurrentDelaySelectorState(currentProxyName: $currentProxyName, isCurrent: $isCurrent, delay: $delay, isInit: $isInit)';
return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UpdateCurrentDelaySelectorStateImpl &&
(identical(other.currentProxyName, currentProxyName) ||
other.currentProxyName == currentProxyName) &&
(identical(other.isCurrent, isCurrent) ||
other.isCurrent == isCurrent) &&
(identical(other.delay, delay) || other.delay == delay) &&
(identical(other.isInit, isInit) || other.isInit == isInit));
other is _$CheckIpSelectorStateImpl &&
(identical(other.isInit, isInit) || other.isInit == isInit) &&
(identical(other.isStart, isStart) || other.isStart == isStart) &&
const DeepCollectionEquality()
.equals(other._selectedMap, _selectedMap));
}
@override
int get hashCode =>
Object.hash(runtimeType, currentProxyName, isCurrent, delay, isInit);
int get hashCode => Object.hash(runtimeType, isInit, isStart,
const DeepCollectionEquality().hash(_selectedMap));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$UpdateCurrentDelaySelectorStateImplCopyWith<
_$UpdateCurrentDelaySelectorStateImpl>
get copyWith => __$$UpdateCurrentDelaySelectorStateImplCopyWithImpl<
_$UpdateCurrentDelaySelectorStateImpl>(this, _$identity);
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith =>
__$$CheckIpSelectorStateImplCopyWithImpl<_$CheckIpSelectorStateImpl>(
this, _$identity);
}
abstract class _UpdateCurrentDelaySelectorState
implements UpdateCurrentDelaySelectorState {
const factory _UpdateCurrentDelaySelectorState(
{required final String? currentProxyName,
required final bool isCurrent,
required final int? delay,
required final bool isInit}) = _$UpdateCurrentDelaySelectorStateImpl;
abstract class _CheckIpSelectorState implements CheckIpSelectorState {
const factory _CheckIpSelectorState(
{required final bool isInit,
required final bool isStart,
required final Map<String, String> selectedMap}) =
_$CheckIpSelectorStateImpl;
@override
String? get currentProxyName;
@override
bool get isCurrent;
@override
int? get delay;
@override
bool get isInit;
@override
bool get isStart;
@override
Map<String, String> get selectedMap;
@override
@JsonKey(ignore: true)
_$$UpdateCurrentDelaySelectorStateImplCopyWith<
_$UpdateCurrentDelaySelectorStateImpl>
_$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
@@ -667,155 +645,6 @@ abstract class _ProfilesSelectorState implements ProfilesSelectorState {
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$PackageListSelectorState {
AccessControl get accessControl => throw _privateConstructorUsedError;
List<Package> get packages => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$PackageListSelectorStateCopyWith<PackageListSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PackageListSelectorStateCopyWith<$Res> {
factory $PackageListSelectorStateCopyWith(PackageListSelectorState value,
$Res Function(PackageListSelectorState) then) =
_$PackageListSelectorStateCopyWithImpl<$Res, PackageListSelectorState>;
@useResult
$Res call({AccessControl accessControl, List<Package> packages});
}
/// @nodoc
class _$PackageListSelectorStateCopyWithImpl<$Res,
$Val extends PackageListSelectorState>
implements $PackageListSelectorStateCopyWith<$Res> {
_$PackageListSelectorStateCopyWithImpl(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? accessControl = null,
Object? packages = null,
}) {
return _then(_value.copyWith(
accessControl: null == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl,
packages: null == packages
? _value.packages
: packages // ignore: cast_nullable_to_non_nullable
as List<Package>,
) as $Val);
}
}
/// @nodoc
abstract class _$$PackageListSelectorStateImplCopyWith<$Res>
implements $PackageListSelectorStateCopyWith<$Res> {
factory _$$PackageListSelectorStateImplCopyWith(
_$PackageListSelectorStateImpl value,
$Res Function(_$PackageListSelectorStateImpl) then) =
__$$PackageListSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({AccessControl accessControl, List<Package> packages});
}
/// @nodoc
class __$$PackageListSelectorStateImplCopyWithImpl<$Res>
extends _$PackageListSelectorStateCopyWithImpl<$Res,
_$PackageListSelectorStateImpl>
implements _$$PackageListSelectorStateImplCopyWith<$Res> {
__$$PackageListSelectorStateImplCopyWithImpl(
_$PackageListSelectorStateImpl _value,
$Res Function(_$PackageListSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accessControl = null,
Object? packages = null,
}) {
return _then(_$PackageListSelectorStateImpl(
accessControl: null == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl,
packages: null == packages
? _value._packages
: packages // ignore: cast_nullable_to_non_nullable
as List<Package>,
));
}
}
/// @nodoc
class _$PackageListSelectorStateImpl implements _PackageListSelectorState {
const _$PackageListSelectorStateImpl(
{required this.accessControl, required final List<Package> packages})
: _packages = packages;
@override
final AccessControl accessControl;
final List<Package> _packages;
@override
List<Package> get packages {
if (_packages is EqualUnmodifiableListView) return _packages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_packages);
}
@override
String toString() {
return 'PackageListSelectorState(accessControl: $accessControl, packages: $packages)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PackageListSelectorStateImpl &&
(identical(other.accessControl, accessControl) ||
other.accessControl == accessControl) &&
const DeepCollectionEquality().equals(other._packages, _packages));
}
@override
int get hashCode => Object.hash(runtimeType, accessControl,
const DeepCollectionEquality().hash(_packages));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl>
get copyWith => __$$PackageListSelectorStateImplCopyWithImpl<
_$PackageListSelectorStateImpl>(this, _$identity);
}
abstract class _PackageListSelectorState implements PackageListSelectorState {
const factory _PackageListSelectorState(
{required final AccessControl accessControl,
required final List<Package> packages}) = _$PackageListSelectorStateImpl;
@override
AccessControl get accessControl;
@override
List<Package> get packages;
@override
@JsonKey(ignore: true)
_$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ApplicationSelectorState {
String? get locale => throw _privateConstructorUsedError;
@@ -2219,3 +2048,159 @@ abstract class _MoreToolsSelectorState implements MoreToolsSelectorState {
_$$MoreToolsSelectorStateImplCopyWith<_$MoreToolsSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$PackageListSelectorState {
AccessControl get accessControl => throw _privateConstructorUsedError;
bool get isAccessControl => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$PackageListSelectorStateCopyWith<PackageListSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PackageListSelectorStateCopyWith<$Res> {
factory $PackageListSelectorStateCopyWith(PackageListSelectorState value,
$Res Function(PackageListSelectorState) then) =
_$PackageListSelectorStateCopyWithImpl<$Res, PackageListSelectorState>;
@useResult
$Res call({AccessControl accessControl, bool isAccessControl});
$AccessControlCopyWith<$Res> get accessControl;
}
/// @nodoc
class _$PackageListSelectorStateCopyWithImpl<$Res,
$Val extends PackageListSelectorState>
implements $PackageListSelectorStateCopyWith<$Res> {
_$PackageListSelectorStateCopyWithImpl(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? accessControl = null,
Object? isAccessControl = null,
}) {
return _then(_value.copyWith(
accessControl: null == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl,
isAccessControl: null == isAccessControl
? _value.isAccessControl
: isAccessControl // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AccessControlCopyWith<$Res> get accessControl {
return $AccessControlCopyWith<$Res>(_value.accessControl, (value) {
return _then(_value.copyWith(accessControl: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$PackageListSelectorStateImplCopyWith<$Res>
implements $PackageListSelectorStateCopyWith<$Res> {
factory _$$PackageListSelectorStateImplCopyWith(
_$PackageListSelectorStateImpl value,
$Res Function(_$PackageListSelectorStateImpl) then) =
__$$PackageListSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({AccessControl accessControl, bool isAccessControl});
@override
$AccessControlCopyWith<$Res> get accessControl;
}
/// @nodoc
class __$$PackageListSelectorStateImplCopyWithImpl<$Res>
extends _$PackageListSelectorStateCopyWithImpl<$Res,
_$PackageListSelectorStateImpl>
implements _$$PackageListSelectorStateImplCopyWith<$Res> {
__$$PackageListSelectorStateImplCopyWithImpl(
_$PackageListSelectorStateImpl _value,
$Res Function(_$PackageListSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accessControl = null,
Object? isAccessControl = null,
}) {
return _then(_$PackageListSelectorStateImpl(
accessControl: null == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl,
isAccessControl: null == isAccessControl
? _value.isAccessControl
: isAccessControl // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$PackageListSelectorStateImpl implements _PackageListSelectorState {
const _$PackageListSelectorStateImpl(
{required this.accessControl, required this.isAccessControl});
@override
final AccessControl accessControl;
@override
final bool isAccessControl;
@override
String toString() {
return 'PackageListSelectorState(accessControl: $accessControl, isAccessControl: $isAccessControl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PackageListSelectorStateImpl &&
(identical(other.accessControl, accessControl) ||
other.accessControl == accessControl) &&
(identical(other.isAccessControl, isAccessControl) ||
other.isAccessControl == isAccessControl));
}
@override
int get hashCode => Object.hash(runtimeType, accessControl, isAccessControl);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl>
get copyWith => __$$PackageListSelectorStateImplCopyWithImpl<
_$PackageListSelectorStateImpl>(this, _$identity);
}
abstract class _PackageListSelectorState implements PackageListSelectorState {
const factory _PackageListSelectorState(
{required final AccessControl accessControl,
required final bool isAccessControl}) = _$PackageListSelectorStateImpl;
@override
AccessControl get accessControl;
@override
bool get isAccessControl;
@override
@JsonKey(ignore: true)
_$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

70
lib/models/ip.dart Normal file
View File

@@ -0,0 +1,70 @@
class IpInfo {
final String ip;
final String countryCode;
const IpInfo({
required this.ip,
required this.countryCode,
});
static IpInfo fromIpInfoIoJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country": final String country,
} =>
IpInfo(
ip: ip,
countryCode: country,
),
_ => throw const FormatException("invalid json"),
};
}
static IpInfo fromIpApiCoJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country_code": final String countryCode,
} =>
IpInfo(
ip: ip,
countryCode: countryCode,
),
_ => throw const FormatException("invalid json"),
};
}
static IpInfo fromIpSbJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country_code": final String countryCode,
} =>
IpInfo(
ip: ip,
countryCode: countryCode,
),
_ => throw const FormatException("invalid json"),
};
}
static IpInfo fromIpwhoIsJson(Map<String, dynamic> json) {
return switch (json) {
{
"ip": final String ip,
"country_code": final String countryCode,
} =>
IpInfo(
ip: ip,
countryCode: countryCode,
),
_ => throw const FormatException("invalid json"),
};
}
@override
String toString() {
return 'IpInfo{ip: $ip, countryCode: $countryCode}';
}
}

View File

@@ -9,8 +9,8 @@ export 'log.dart';
export 'system_color_scheme.dart';
export 'connection.dart';
export 'package.dart';
export 'common.dart';
export 'ffi.dart';
export 'selector.dart';
export 'navigation.dart';
export 'dav.dart';
export 'dav.dart';
export 'ip.dart';

View File

@@ -11,6 +11,7 @@ class NavigationItem with _$NavigationItem {
required String label,
final String? description,
required Widget fragment,
@Default(true) bool keep,
String? path,
@Default([NavigationItemMode.mobile, NavigationItemMode.desktop])
List<NavigationItemMode> modes,

View File

@@ -5,11 +5,8 @@ import 'dart:typed_data';
import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:json_annotation/json_annotation.dart';
import 'common.dart';
part 'generated/profile.g.dart';
typedef SelectedMap = Map<String, String>;
@@ -19,16 +16,17 @@ class UserInfo {
int upload;
int download;
int total;
int? expire;
int expire;
UserInfo({
int? upload,
int? download,
int? total,
this.expire,
int? expire,
}) : upload = upload ?? 0,
download = download ?? 0,
total = total ?? 0;
total = total ?? 0,
expire = expire ?? 0;
Map<String, dynamic> toJson() {
return _$UserInfoToJson(this);
@@ -40,10 +38,10 @@ class UserInfo {
factory UserInfo.formHString(String? info) {
if (info == null) return UserInfo();
var list = info.split(";");
final list = info.split(";");
Map<String, int?> map = {};
for (var i in list) {
var keyValue = i.trim().split("=");
for (final i in list) {
final keyValue = i.trim().split("=");
map[keyValue[0]] = int.tryParse(keyValue[1]);
}
return UserInfo(
@@ -83,44 +81,29 @@ class Profile {
Duration? autoUpdateDuration,
this.autoUpdate = true,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
autoUpdateDuration =
autoUpdateDuration ?? defaultUpdateDuration,
autoUpdateDuration = autoUpdateDuration ?? defaultUpdateDuration,
selectedMap = selectedMap ?? {};
ProfileType get type => url == null ? ProfileType.file : ProfileType.url;
ProfileType get type =>
url == null || url?.isEmpty == true ? ProfileType.file : ProfileType.url;
Future<Result<bool>> checkAndUpdate() async {
Future<void> checkAndUpdate() async {
final isExists = await check();
if(!isExists){
if(url != null){
if (!isExists) {
if (url != null) {
return await update();
}
return Result.error();
}
return Result.success();
}
Future<Result<bool>> update() async {
if (url == null) {
return Result.error(
appLocalizations.unableToUpdateCurrentProfileDesc,
);
}
final responseResult = await Request.getFileResponseForUrl(url!);
final response = responseResult.data;
if (responseResult.type != ResultType.success || response == null) {
return Result.error(responseResult.message);
}
final disposition = response.headers['content-disposition'];
Future<void> update() async {
final response = await request.getFileResponseForUrl(url!);
final disposition = response.headers.value("content-disposition");
label ??= other.getFileNameForDisposition(disposition) ?? id;
final userinfo = response.headers['subscription-userinfo'];
final userinfo = response.headers.value('subscription-userinfo');
userInfo = UserInfo.formHString(userinfo);
final saveResult = await saveFile(response.bodyBytes);
if (saveResult.type == ResultType.error) {
return Result.error(saveResult.message);
}
await saveFile(response.data);
lastUpdateDate = DateTime.now();
return Result.success();
}
Future<bool> check() async {
@@ -128,10 +111,10 @@ class Profile {
return await File(profilePath!).exists();
}
Future<Result<void>> saveFile(Uint8List bytes) async {
final isValidate = clashCore.validateConfig(utf8.decode(bytes));
if (!isValidate) {
return Result.error(appLocalizations.profileParseErrorDesc);
Future<void> saveFile(Uint8List bytes) async {
final message = await clashCore.validateConfig(utf8.decode(bytes));
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
@@ -141,7 +124,6 @@ class Profile {
}
await file.writeAsBytes(bytes);
lastUpdateDate = DateTime.now();
return Result.success();
}
Map<String, dynamic> toJson() {

View File

@@ -1,11 +1,7 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'config.dart';
import 'navigation.dart';
import 'package.dart';
import 'profile.dart';
import 'proxy.dart';
part 'generated/selector.freezed.dart';
@@ -18,13 +14,12 @@ class StartButtonSelectorState with _$StartButtonSelectorState {
}
@freezed
class UpdateCurrentDelaySelectorState with _$UpdateCurrentDelaySelectorState {
const factory UpdateCurrentDelaySelectorState({
required String? currentProxyName,
required bool isCurrent,
required int? delay,
class CheckIpSelectorState with _$CheckIpSelectorState {
const factory CheckIpSelectorState({
required bool isInit,
}) = _UpdateCurrentDelaySelectorState;
required bool isStart,
required SelectedMap selectedMap,
}) = _CheckIpSelectorState;
}
@freezed
@@ -44,14 +39,6 @@ class ProfilesSelectorState with _$ProfilesSelectorState {
}) = _ProfilesSelectorState;
}
@freezed
class PackageListSelectorState with _$PackageListSelectorState {
const factory PackageListSelectorState({
required AccessControl accessControl,
required List<Package> packages,
}) = _PackageListSelectorState;
}
@freezed
class ApplicationSelectorState with _$ApplicationSelectorState {
const factory ApplicationSelectorState({
@@ -62,24 +49,23 @@ class ApplicationSelectorState with _$ApplicationSelectorState {
}
@freezed
class TrayContainerSelectorState with _$TrayContainerSelectorState{
class TrayContainerSelectorState with _$TrayContainerSelectorState {
const factory TrayContainerSelectorState({
required Mode mode,
required bool autoLaunch,
required bool isRun,
required String? locale,
})=_TrayContainerSelectorState;
}) = _TrayContainerSelectorState;
}
@freezed
class UpdateNavigationsSelector with _$UpdateNavigationsSelector{
class UpdateNavigationsSelector with _$UpdateNavigationsSelector {
const factory UpdateNavigationsSelector({
required bool openLogs,
required bool hasProxies,
}) = _UpdateNavigationsSelector;
}
@freezed
class HomeSelectorState with _$HomeSelectorState {
const factory HomeSelectorState({
@@ -98,21 +84,21 @@ class HomeBodySelectorState with _$HomeBodySelectorState {
}
@freezed
class ProxiesCardSelectorState with _$ProxiesCardSelectorState{
class ProxiesCardSelectorState with _$ProxiesCardSelectorState {
const factory ProxiesCardSelectorState({
required bool isSelected,
}) = _ProxiesCardSelectorState;
}
@freezed
class ProxiesSelectorState with _$ProxiesSelectorState{
class ProxiesSelectorState with _$ProxiesSelectorState {
const factory ProxiesSelectorState({
required List<String> groupNames,
}) = _ProxiesSelectorState;
}
@freezed
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState{
class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState {
const factory ProxiesTabViewSelectorState({
required ProxiesSortType proxiesSortType,
required num sortNum,
@@ -126,4 +112,12 @@ class MoreToolsSelectorState with _$MoreToolsSelectorState {
const factory MoreToolsSelectorState({
required List<NavigationItem> navigationItems,
}) = _MoreToolsSelectorState;
}
}
@freezed
class PackageListSelectorState with _$PackageListSelectorState {
const factory PackageListSelectorState({
required AccessControl accessControl,
required bool isAccessControl,
}) = _PackageListSelectorState;
}

View File

@@ -1,3 +1,4 @@
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';
@@ -56,54 +57,65 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopContainer(
child: Selector2<AppState, Config, HomeSelectorState>(
selector: (_, appState, config) => HomeSelectorState(
currentLabel: appState.currentLabel,
navigationItems: appState.currentNavigationItems,
viewMode: appState.viewMode,
locale: config.locale,
),
builder: (_, state, child) {
final viewMode = state.viewMode;
final navigationItems = state.navigationItems;
final currentLabel = state.currentLabel;
final index = navigationItems.lastIndexWhere(
(element) => element.label == currentLabel,
);
final currentIndex = index == -1 ? 0 : index;
final navigationBar = _getNavigationBar(
viewMode: viewMode,
navigationItems: navigationItems,
currentIndex: currentIndex,
);
final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null;
Widget body;
if (viewMode != ViewMode.mobile) {
body = Row(
children: [
navigationBar,
Expanded(
flex: 1,
child: child!,
)
],
);
} else {
body = child!;
child: LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
return Selector2<AppState, Config, HomeSelectorState>(
selector: (_, appState, config) {
return HomeSelectorState(
currentLabel: appState.currentLabel,
navigationItems: appState.currentNavigationItems,
viewMode: other.getViewMode(maxWidth),
locale: config.locale,
);
},
builder: (_, state, child) {
final viewMode = state.viewMode;
final navigationItems = state.navigationItems;
final currentLabel = state.currentLabel;
final index = navigationItems.lastIndexWhere(
(element) => element.label == currentLabel,
);
final currentIndex = index == -1 ? 0 : index;
final navigationBar = _getNavigationBar(
viewMode: viewMode,
navigationItems: navigationItems,
currentIndex: currentIndex,
);
final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null;
Widget body;
if (viewMode != ViewMode.mobile) {
body = Row(
children: [
navigationBar,
Expanded(
flex: 1,
child: child!,
)
],
);
} else {
body = child!;
}
return CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
),
body: body,
bottomNavigationBar: bottomNavigationBar,
);
},
child: const HomeBody(
key: Key("home_boy"),
),
body: body,
bottomNavigationBar: bottomNavigationBar,
);
},
child: const HomeBody(
key: Key("home_boy"),
),
),
);
}
@@ -120,7 +132,7 @@ class HomeBody extends StatelessWidget {
final currentIndex = index == -1 ? 0 : index;
if (globalState.pageController != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.toPage(currentIndex);
globalState.appController.toPage(currentIndex, hasAnimate: true);
});
} else {
globalState.pageController = PageController(
@@ -146,6 +158,7 @@ class HomeBody extends StatelessWidget {
itemBuilder: (_, index) {
final navigationItem = navigationItems[index];
return KeepContainer(
keep: navigationItem.keep,
key: Key(navigationItem.label),
child: navigationItem.fragment,
);

View File

@@ -107,6 +107,9 @@ class _ScanPageState extends State<ScanPage> with WidgetsBindingObserver {
case TorchState.unavailable:
icon = const Icon(Icons.flash_off);
backgroundColor = Colors.transparent;
case TorchState.auto:
icon = const Icon(Icons.flash_auto);
backgroundColor = Colors.orange;
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -17,10 +18,8 @@ class App {
methodChannel = const MethodChannel("app");
methodChannel!.setMethodCallHandler((call) async {
switch (call.method) {
case "exit":
if (onExit != null) {
await onExit!();
}
case "gc":
clashCore.requestGc();
break;
default:
throw MissingPluginException();
@@ -29,10 +28,6 @@ class App {
}
}
setOnExit(Function() onExit) {
this.onExit = onExit;
}
factory App() {
_instance ??= App._internal();
return _instance!;

View File

@@ -70,7 +70,7 @@ class Proxy extends ProxyPlatform {
});
}
bool get isStart => startTime != null && startTime!.isBeforeNow();
bool get isStart => startTime != null && startTime!.isBeforeNow;
startAfterHook(int? fd) {
if (!isStart && fd != null) {

View File

@@ -13,7 +13,6 @@ import 'common/common.dart';
class GlobalState {
Timer? timer;
Function? healthcheckLockDebounce;
Timer? groupsUpdateTimer;
Function? updateCurrentDelayDebounce;
PageController? pageController;
@@ -22,7 +21,6 @@ class GlobalState {
late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
List<Function> updateFunctionLists = [];
bool healthcheckLock = false;
startListenUpdate() {
if (timer != null && timer!.isActive == true) return;
@@ -38,7 +36,7 @@ class GlobalState {
timer?.cancel();
}
Future<String> updateClashConfig({
Future<void> updateClashConfig({
required ClashConfig clashConfig,
required Config config,
bool isPatch = true,
@@ -46,12 +44,13 @@ class GlobalState {
final profilePath = await appPath.getProfilePath(config.currentProfileId);
await config.currentProfile?.checkAndUpdate();
debugPrint("update config");
return clashCore.updateConfig(UpdateConfigParams(
final res = await clashCore.updateConfig(UpdateConfigParams(
profilePath: profilePath,
config: clashConfig,
isPatch: isPatch,
isCompatible: config.isCompatible,
));
if (res.isNotEmpty) throw res;
}
updateCoreVersionInfo(AppState appState) {
@@ -62,8 +61,14 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) async {
final args =
config.isAccessControl ? json.encode(config.accessControl) : null;
final args = config.isAccessControl
? json.encode(
Props(
accessControl: config.accessControl,
allowBypass: config.allowBypass,
),
)
: null;
await proxyManager.startProxy(
port: clashConfig.mixedPort,
args: args,
@@ -76,17 +81,16 @@ class GlobalState {
stopListenUpdate();
}
applyProfile({
Future<void> applyProfile({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) async {
final res = await updateClashConfig(
await updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: false,
);
if (res.isNotEmpty) return Result.error(res);
await updateGroups(appState);
changeProxy(
appState: appState,
@@ -107,12 +111,6 @@ class GlobalState {
clashConfig: clashConfig,
);
}
if (!appState.isInit) return;
await applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
updateCoreVersionInfo(appState);
}
@@ -121,35 +119,20 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
}
updateNavigationItems({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) {
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
appState.navigationItems = navigation.getItems(
openLogs: config.openLogs,
hasProxies: group.isNotEmpty && hasProfile,
);
}
Future<void> updateGroups(AppState appState) async {
appState.groups = await clashCore.getProxiesGroups();
}
@@ -167,9 +150,7 @@ class GlobalState {
title: Text(title),
content: Container(
width: 300,
constraints: const BoxConstraints(
maxHeight: 200
),
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: RichText(
overflow: TextOverflow.visible,
@@ -257,18 +238,22 @@ class GlobalState {
);
}
void updateCurrentDelay(
String? proxyName,
) {
updateCurrentDelayDebounce ??= debounce<Function(String?)>((proxyName) {
if (proxyName != null) {
debugPrint("[delay]=====> $proxyName");
clashCore.delay(
proxyName,
);
}
});
updateCurrentDelayDebounce!([proxyName]);
Future<T?> safeRun<T>(
FutureOr<T> Function() futureFunction, {
String? title,
}) async {
try {
final res = await futureFunction();
return res;
} catch (e) {
showMessage(
title: title ?? appLocalizations.tip,
message: TextSpan(
text: e.toString(),
),
);
return null;
}
}
}

View File

@@ -38,16 +38,8 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
@override
void onDelay(Delay delay) {
globalState.healthcheckLock = true;
final appController = globalState.appController;
appController.setDelay(delay);
globalState.healthcheckLockDebounce ??= debounce<Function()>(
() async {
globalState.healthcheckLock = false;
},
milliseconds: 5000,
);
globalState.healthcheckLockDebounce!();
super.onDelay(delay);
}

View File

@@ -2,8 +2,13 @@ import 'package:flutter/material.dart';
class KeepContainer extends StatefulWidget {
final Widget child;
final bool keep;
const KeepContainer({super.key, required this.child});
const KeepContainer({
super.key,
required this.child,
this.keep = true,
});
@override
State<KeepContainer> createState() => _KeepContainerState();
@@ -11,7 +16,6 @@ class KeepContainer extends StatefulWidget {
class _KeepContainerState extends State<KeepContainer>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
@@ -19,5 +23,5 @@ class _KeepContainerState extends State<KeepContainer>
}
@override
bool get wantKeepAlive => true;
bool get wantKeepAlive => widget.keep;
}

View File

@@ -71,6 +71,7 @@ class ListItem<T> extends StatelessWidget {
final Widget title;
final Widget? subtitle;
final EdgeInsets padding;
final ListTileTitleAlignment tileTitleAlignment;
final bool? prue;
final Widget? trailing;
final Delegate delegate;
@@ -87,6 +88,7 @@ class ListItem<T> extends StatelessWidget {
this.horizontalTitleGap,
this.prue,
this.onTab,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : delegate = const Delegate();
const ListItem.open({
@@ -99,6 +101,7 @@ class ListItem<T> extends StatelessWidget {
required OpenDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
const ListItem.next({
@@ -111,6 +114,7 @@ class ListItem<T> extends StatelessWidget {
required NextDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
const ListItem.checkbox({
@@ -122,6 +126,7 @@ class ListItem<T> extends StatelessWidget {
required CheckboxDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
@@ -134,6 +139,7 @@ class ListItem<T> extends StatelessWidget {
required SwitchDelegate this.delegate,
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
@@ -146,6 +152,7 @@ class ListItem<T> extends StatelessWidget {
required RadioDelegate<T> this.delegate,
this.horizontalTitleGap = 8,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : leading = null,
onTab = null;
@@ -193,6 +200,7 @@ class ListItem<T> extends StatelessWidget {
horizontalTitleGap: horizontalTitleGap,
title: title,
subtitle: subtitle,
titleAlignment: tileTitleAlignment,
onTap: onTab,
trailing: trailing ?? this.trailing,
contentPadding: padding,

View File

@@ -49,6 +49,7 @@ 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);
@@ -58,11 +59,16 @@ 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,
}) async {
if (_loading.value == true) return null;
_loading.value = true;
try {
final res = await futureFunction();
@@ -85,6 +91,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
@@ -109,6 +116,12 @@ class CommonScaffoldState extends State<CommonScaffold> {
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
floatingActionButton: ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
},
),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(

View File

@@ -27,11 +27,6 @@ class _WindowContainerState extends State<WindowContainer>
windowManager.addListener(this);
}
@override
void onWindowResize() {
globalState.appController.updateViewWidth();
}
@override
void onWindowClose() async {
await globalState.appController.handleBackOrExit();

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
version: 0.8.7
version: 0.8.21
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -17,15 +17,14 @@ dependencies:
provider: ^6.0.5
window_manager: ^0.3.8
ffi: ^2.1.0
dynamic_color: ^1.7.0
dynamic_color: ^1.6.0
proxy:
path: plugins/proxy
launch_at_startup: ^0.2.2
windows_single_instance: ^1.0.1
json_annotation: ^4.9.0
http: ^1.2.0
file_picker: ^6.1.1
mobile_scanner: 5.0.1
file_picker: ^8.0.3
mobile_scanner: ^5.1.1
app_links: ^3.5.0
win32_registry: ^1.1.2
tray_manager: ^0.2.1
@@ -34,10 +33,12 @@ dependencies:
package_info_plus: ^7.0.0
url_launcher: ^6.2.6
freezed_annotation: ^2.4.1
image_picker: ^1.1.1
image_picker: ^1.1.2
zxing2: ^0.2.3
image: ^4.1.7
webdav_client: ^1.2.2
dio: ^5.4.3+1
country_flags: ^2.2.0
dev_dependencies:
flutter_test:
sdk: flutter
@@ -51,7 +52,7 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
- assets/data/geoip.metadb
- assets/data/
- assets/images/
ffigen:
name: "ClashFFI"

View File

@@ -12,10 +12,7 @@ enum PlatformType {
macos,
}
enum Arch {
amd64,
arm64,
}
enum Arch { amd64, arm64, arm }
class BuildLibItem {
PlatformType platform;
@@ -64,6 +61,11 @@ class Build {
arch: Arch.amd64,
archName: 'amd64',
),
BuildLibItem(
platform: PlatformType.android,
arch: Arch.arm,
archName: 'armeabi-v7a',
),
BuildLibItem(
platform: PlatformType.android,
arch: Arch.arm64,
@@ -334,7 +336,7 @@ class BuildCommand extends Command {
final archName = argResults?['arch'];
final currentArches =
arches.where((element) => element.name == archName).toList();
final arch = currentArches.isEmpty ? null : arches.first;
final arch = currentArches.isEmpty ? null : currentArches.first;
await _buildLib(arch);
if (build != "all") {
return;
@@ -357,10 +359,11 @@ class BuildCommand extends Command {
break;
case PlatformType.android:
final targetMap = {
Arch.arm: "android-arm",
Arch.arm64: "android-arm64",
Arch.amd64: "android-x64",
Arch.arm64: "android-arm64"
};
final defaultArches = [Arch.amd64, Arch.arm64];
final defaultArches = [Arch.arm, Arch.arm64, Arch.amd64];
final defaultTargets = defaultArches
.where((element) => arch == null ? true : element == arch)
.map((e) => targetMap[e])