Compare commits

..

14 Commits

Author SHA1 Message Date
chen08209
f679d1e6d8 Optimize request ua
Change android icon

Optimize dashboard
2024-07-20 18:04:59 +08:00
chen08209
f2aa8851ae Remove request validate certificate
Sync core
2024-07-18 21:28:27 +08:00
chen08209
ec2890cab2 Fix windows error 2024-07-18 17:27:29 +08:00
chen08209
ca946c1b06 Fix setup.dart error 2024-07-18 16:39:28 +08:00
chen08209
3bc3172723 Fix android system proxy not effective
Add macos arm64
2024-07-18 16:33:53 +08:00
chen08209
82be4cc45f Optimize proxies page
Support mouse drag scroll

Adjust desktop ui
2024-07-17 14:52:15 +08:00
chen08209
2c3f4ae8a8 Revert "Fix android vpn issues"
This reverts commit 891977408e.
2024-07-17 14:51:52 +08:00
chen08209
891977408e Fix android vpn issues 2024-07-15 16:19:58 +08:00
chen08209
5292f34e8d Fix android vpn issues 2024-07-15 16:18:51 +08:00
chen08209
1c54db6bf3 Rollback partial modification 2024-07-15 16:14:19 +08:00
chen08209
a4b5f4abdb Fix the problem that ui can't be synchronized when android vpn is occupied by an external
Override default socksPort,port
2024-07-15 13:47:06 +08:00
chen08209
aa4ffbe4fb Fix fab issues 2024-07-14 01:37:44 +08:00
chen08209
3d25298639 Update version 2024-07-14 00:05:42 +08:00
chen08209
1765576d09 Fix the problem that vpn cannot be started in some cases
Fix the problem that geodata url does not take effect
2024-07-14 00:01:18 +08:00
102 changed files with 3463 additions and 1986 deletions

View File

@@ -15,10 +15,16 @@ jobs:
os: ubuntu-latest
- platform: windows
os: windows-latest
arch: amd64
- platform: linux
os: ubuntu-latest
arch: amd64
- platform: macos
os: macos-13
arch: amd64
- platform: macos
os: macos-latest
arch: arm64
steps:
- name: Setup Mingw64
@@ -89,13 +95,12 @@ jobs:
run: flutter pub get
- name: Setup
run: |
dart setup.dart ${{ matrix.platform }}
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
- name: Upload
uses: actions/upload-artifact@v4
with:
name: artifact-${{ matrix.platform }}
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
path: ./dist
retention-days: 1
overwrite: true

View File

@@ -24,8 +24,9 @@
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true"
android:label="FlClash"
tools:targetApi="n">
tools:targetApi="tiramisu">
<activity
android:name="com.follow.clash.MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -74,7 +75,7 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:icon="@drawable/icon"
android:icon="@drawable/ic_stat_name"
android:foregroundServiceType="specialUse"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,7 +1,7 @@
package com.follow.clash
import android.app.Activity
import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
@@ -9,7 +9,8 @@ import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import java.util.Date
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
enum class RunState {
START,
@@ -17,7 +18,11 @@ enum class RunState {
STOP
}
object GlobalState {
private val lock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null
@@ -39,19 +44,22 @@ object GlobalState {
fun initServiceEngine(context: Context) {
if (serviceEngine != null) return
val serviceEngine = FlutterEngine(context)
serviceEngine.plugins.add(ProxyPlugin())
serviceEngine.plugins.add(AppPlugin())
serviceEngine.plugins.add(TilePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
)
serviceEngine.dartExecutor.executeDartEntrypoint(
vpnService,
listOf("${flutterEngine == null}")
)
GlobalState.serviceEngine = serviceEngine
lock.withLock {
destroyServiceEngine()
serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(ProxyPlugin())
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService"
)
serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService,
)
Log.e("FlClashVpnService", "initServiceEngine ===>")
}
}
}

View File

@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
@@ -46,6 +47,8 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
flClashVpnService = binder.getService()
if (isStart) {
startVpn()
} else {
flClashVpnService?.initServiceEngine()
}
}
@@ -129,7 +132,13 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
}
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
flutterMethodChannel.invokeMethod("started", flClashVpnService?.start(port, props))
val intent = VpnService.prepare(context)
if (intent != null) {
stopVpn()
return
}
val fd = flClashVpnService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
}
private fun stopVpn() {

View File

@@ -71,7 +71,7 @@ class FlClashTileService : TileService() {
if (titlePlugin != null) {
titlePlugin.handleStart()
} else {
GlobalState.initServiceEngine(this)
GlobalState.initServiceEngine(applicationContext)
}
} else if (GlobalState.runState.value == RunState.START) {
GlobalState.runState.value = RunState.PENDING

View File

@@ -11,12 +11,18 @@ import android.net.VpnService
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.Props
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlClashVpnService : VpnService() {
@@ -114,7 +120,6 @@ class FlClashVpnService : VpnService() {
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
@@ -127,12 +132,12 @@ class FlClashVpnService : VpnService() {
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true);
setAutoCancel(true)
}
}
fun initServiceEngine() {
GlobalState.initServiceEngine(this)
GlobalState.initServiceEngine(applicationContext)
}
override fun onTrimMemory(level: Int) {
@@ -169,8 +174,23 @@ class FlClashVpnService : VpnService() {
inner class LocalBinder : Binder() {
fun getService(): FlClashVpnService = this@FlClashVpnService
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
try {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
}
return isSuccess
} catch (e: RemoteException) {
throw e
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}

View File

@@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="240"
android:viewportHeight="240"
android:tint="#FFFFFF">
<group android:scaleX="2.0264318"
android:scaleY="2.0264318"
android:translateX="-123.17181"
android:translateY="-123.17181">
<group android:scaleX="0.61"
android:scaleY="0.61"
android:translateX="46.8"
android:translateY="46.8">
<path
android:pathData="M67.67,115.76L144.04,39.39A18,18 0,0 1,169.5 39.39L169.5,39.39A18,18 0,0 1,169.5 64.85L93.13,141.21A18,18 0,0 1,67.67 141.21L67.67,141.21A18,18 0,0 1,67.67 115.76z"
android:fillColor="#6666FF"/>
<path
android:pathData="M98.79,146.87L136.97,108.69A18,18 72.93,0 1,162.43 108.69L162.43,108.69A18,18 72.93,0 1,162.43 134.14L124.24,172.33A18,18 76.87,0 1,98.79 172.33L98.79,172.33A18,18 76.87,0 1,98.79 146.87z"
android:fillColor="#326CBF"/>
<path
android:pathData="M129.9,177.98L129.9,177.98A18,18 107.07,0 1,155.36 177.98L155.36,177.98A18,18 107.07,0 1,155.36 203.44L155.36,203.44A18,18 107.07,0 1,129.9 203.44L129.9,203.44A18,18 107.07,0 1,129.9 177.98z"
android:fillColor="#56afee"/>
</group>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="240"
android:viewportHeight="240">
<group android:scaleX="0.90244895"
android:scaleY="0.90244895"
android:translateX="12.708843"
android:translateY="10.703402">
<group android:scaleX="0.61"
android:scaleY="0.61"
android:translateX="46.8"
android:translateY="46.8">
<path
android:pathData="M67.67,115.76L144.04,39.39A18,18 0,0 1,169.5 39.39L169.5,39.39A18,18 0,0 1,169.5 64.85L93.13,141.21A18,18 0,0 1,67.67 141.21L67.67,141.21A18,18 0,0 1,67.67 115.76z"
android:fillColor="#6666FF"/>
<path
android:pathData="M98.79,146.87L136.97,108.69A18,18 72.93,0 1,162.43 108.69L162.43,108.69A18,18 72.93,0 1,162.43 134.14L124.24,172.33A18,18 76.87,0 1,98.79 172.33L98.79,172.33A18,18 76.87,0 1,98.79 146.87z"
android:fillColor="#326CBF"/>
<path
android:pathData="M129.9,177.98L129.9,177.98A18,18 107.07,0 1,155.36 177.98L155.36,177.98A18,18 107.07,0 1,155.36 203.44L155.36,203.44A18,18 107.07,0 1,129.9 203.44L129.9,203.44A18,18 107.07,0 1,129.9 177.98z"
android:fillColor="#56afee"/>
</group>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -6,7 +6,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">#121212</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

View File

@@ -6,7 +6,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#EFEFEF</color>
<color name="ic_launcher_background">#FAFAFA</color>
</resources>

View File

@@ -337,6 +337,8 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA
if targetConfig.DNS.Enable == false {
targetConfig.DNS = patchConfig.DNS
}

View File

@@ -7,8 +7,8 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
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.25.0
github.com/miekg/dns v1.1.61
golang.org/x/net v0.26.0
golang.org/x/sync v0.7.0
)
@@ -31,7 +31,7 @@ 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/chi/v5 v5.0.14 // 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
@@ -44,10 +44,10 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240419123447-f1cffa2c0c49 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -55,13 +55,14 @@ 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.43.2-0.20240518033621-2c3d14c6b38e // indirect
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
github.com/metacubex/randv2 v0.2.0 // 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.7-0.20240512075008-89e7c6208eec // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e // 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/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // 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
@@ -71,18 +72,19 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
github.com/sagernet/sing v0.3.8 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.0-alpha.10 // indirect
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/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // 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
@@ -91,21 +93,20 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zhangyunhao116/fastrand v0.4.0 // indirect
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.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.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

View File

@@ -48,8 +48,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/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/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/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=
@@ -75,7 +75,6 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
@@ -85,16 +84,16 @@ github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7s
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20240419123447-f1cffa2c0c49 h1:/OuvSMGT9+xnyZ+7MZQ1zdngaCCAdPoSw8B/uurZ7pg=
github.com/insomniacslk/dhcp v0.0.0-20240419123447-f1cffa2c0c49/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 h1:dh8D8FksyMhD64mRMbUhZHWYJfNoNMCxfVq6eexleMw=
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -113,26 +112,28 @@ 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.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/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvWDcBDAkIv5kUYIhzHwafDVq635BuybnKqI=
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
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.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-tun v0.2.7-0.20240627012306-9d1f5fc0b45e h1:o+zohxPRo45P35fS9u1zfdBgr+L/7S0ObGU6YjbVBIc=
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM=
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/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/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/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
@@ -155,8 +156,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
@@ -165,11 +166,13 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.3.8 h1:gm4JKalPhydMYX2zFOTnnd4TXtM/16WFRqSjMepYQQk=
github.com/sagernet/sing v0.3.8/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI=
github.com/sagernet/sing v0.5.0-alpha.10 h1:kuHl10gpjbKQAdQfyogQU3u0CVnpqC3wrAHe/+BFaXc=
github.com/sagernet/sing v0.5.0-alpha.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
@@ -180,8 +183,8 @@ github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2F
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.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
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=
@@ -195,15 +198,9 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -215,14 +212,12 @@ github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zhangyunhao116/fastrand v0.4.0 h1:86QB6Y+GGgLZRFRDCjMmAS28QULwspK9sgL5d1Bx3H4=
github.com/zhangyunhao116/fastrand v0.4.0/go.mod h1:vIyo6EyBhjGKpZv6qVlkPl4JVAklpMM4DSKzbAkMguA=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
@@ -231,18 +226,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.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/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
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=
@@ -262,26 +257,24 @@ 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.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
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.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 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=

43
core/status.go Normal file
View File

@@ -0,0 +1,43 @@
//go:build android
package main
import "C"
import (
"encoding/json"
"fmt"
)
type AccessControl struct {
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
}
type AndroidProps struct {
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
}
var androidProps AndroidProps
//export getAndroidProps
func getAndroidProps() *C.char {
data, err := json.Marshal(androidProps)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export setAndroidProps
func setAndroidProps(s *C.char) {
paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), &androidProps)
if err != nil {
return
}
}

View File

@@ -137,55 +137,59 @@ class ApplicationState extends State<Application> {
Widget build(context) {
return AppStateContainer(
child: ClashMessageContainer(
child: _buildApp(
Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales:
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
home: child,
);
},
);
},
child: const HomePage(),
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return PopContainer(
child: _buildApp(child!),
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
home: child,
);
},
);
},
child: const HomePage(),
),
),
);

View File

@@ -237,6 +237,21 @@ class ClashCore {
return VersionInfo.fromJson(versionInfo);
}
setProps(Props props) {
final propsChar = json.encode(props).toNativeUtf8().cast<Char>();
clashFFI.setAndroidProps(propsChar);
malloc.free(propsChar);
}
Props getProps() {
final androidPropsRaw = clashFFI.getAndroidProps();
final androidProps = json.decode(
androidPropsRaw.cast<Utf8>().toDartString(),
);
clashFFI.freeCString(androidPropsRaw);
return Props.fromJson(androidProps);
}
Traffic getTraffic() {
final trafficRaw = clashFFI.getTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());

View File

@@ -5499,6 +5499,30 @@ class ClashFFI {
late final _setProcessMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getAndroidProps() {
return _getAndroidProps();
}
late final _getAndroidPropsPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getAndroidProps');
late final _getAndroidProps =
_getAndroidPropsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void setAndroidProps(
ffi.Pointer<ffi.Char> s,
) {
return _setAndroidProps(
s,
);
}
late final _setAndroidPropsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setAndroidProps');
late final _setAndroidProps =
_setAndroidPropsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startTUN(
int fd,
int port,

View File

@@ -24,4 +24,5 @@ export 'function.dart';
export 'package.dart';
export 'measure.dart';
export 'service.dart';
export 'iterable.dart';
export 'iterable.dart';
export 'scroll.dart';

View File

@@ -1,7 +1,10 @@
import 'dart:io';
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash";
const coreName = "clash.meta";
@@ -14,11 +17,16 @@ const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0;
const GeoXMap defaultGeoXMap = {
"mmdb":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
"geosite":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
@@ -39,4 +47,10 @@ final filter = ImageFilter.blur(
tileMode: TileMode.mirror,
);
const viewModeColumnsMap = {
ViewMode.mobile: [2, 1],
ViewMode.laptop: [3, 2],
ViewMode.desktop: [4, 3],
};
const defaultPrimaryColor = Colors.brown;

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
@@ -189,6 +190,24 @@ class Other {
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop;
}
int getColumns(ViewMode viewMode, int currentColumns) {
final targetColumnsArray = viewModeColumnsMap[viewMode]!;
if (targetColumnsArray.contains(currentColumns)) {
return currentColumns;
}
return targetColumnsArray.first;
}
String getColumnsTextForInt(int number){
return switch(number){
1 => appLocalizations.oneColumn,
2 => appLocalizations.twoColumns,
3 => appLocalizations.threeColumns,
4 => appLocalizations.fourColumns,
int() => throw UnimplementedError(),
};
}
}
final other = Other();

View File

@@ -15,8 +15,8 @@ class ProxyManager {
DateTime? get startTime => _proxy.startTime;
Future<bool?> startProxy({required int port, String? args}) async {
return await _proxy.startProxy(port, args);
Future<bool?> startProxy({required int port}) async {
return await _proxy.startProxy(port);
}
Future<bool?> stopProxy() async {

View File

@@ -19,14 +19,14 @@ class Request {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
_updateAdapter();
return handler.next(options); // 继续请求
},
),
);
}
_syncProxy() {
_updateAdapter() {
final port = globalState.appController.clashConfig.mixedPort;
final isStart = globalState.appController.appState.isStart;
if (_port != port || isStart != _isStart) {
@@ -36,11 +36,13 @@ class Request {
createHttpClient: () {
final client = HttpClient();
if (!_isStart) return client;
client.userAgent = globalState.appController.clashConfig.globalUa;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
return client;
},
validateCertificate: (_, __, ___) => true,
);
}
}

16
lib/common/scroll.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
class BaseScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
PointerDeviceKind.trackpad,
if (system.isDesktop) PointerDeviceKind.mouse,
PointerDeviceKind.unknown,
};
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';
@@ -8,7 +9,7 @@ import 'protocol.dart';
import 'system.dart';
class Window {
init() async {
init(WindowProps props) async {
if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
protocol.register("clash");
@@ -16,11 +17,21 @@ class Window {
protocol.register("flclash");
}
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
size: Size(1000, 600),
minimumSize: Size(400, 600),
center: true,
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 600),
titleBarStyle: TitleBarStyle.hidden,
);
if (props.left != null || props.top != null) {
await windowManager.setPosition(
Offset(props.left ?? 0, props.top ?? 0),
);
} else {
await windowManager.setAlignment(Alignment.center);
}
// if(Platform.isWindows){
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
// }
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true);
});

View File

@@ -411,14 +411,7 @@ class AppController {
}
int get columns =>
globalState.getColumns(appState.viewMode, config.proxiesColumns);
changeColumns() {
config.proxiesColumns = globalState.getColumns(
appState.viewMode,
columns - 1,
);
}
other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {

View File

@@ -82,6 +82,6 @@ enum ChipType { action, delete }
enum CommonCardType { plain, filled }
enum ProxiesType { tab, expansion }
enum ProxiesType { tab, list }
enum ProxyCardType { expand, shrink }
enum ProxyCardType { expand, shrink, min }

View File

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

View File

@@ -1,11 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -63,9 +61,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
},
);
@@ -162,7 +157,12 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
onBlock: _handleBlockConnection,
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
@@ -181,113 +181,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Function(String)? onBlock;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.onBlock,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid
? Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
if (onBlock == null) return;
onBlock!(connection.id);
},
),
);
}
}
class ConnectionsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
@@ -333,11 +226,11 @@ class ConnectionsSearchDelegate extends SearchDelegate {
);
}
_handleBlockConnection(String id) {
clashCore.closeConnections(id);
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
);
}
@override
@@ -417,7 +310,12 @@ class ConnectionsSearchDelegate extends SearchDelegate {
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
onBlock: _handleBlockConnection,
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {

View File

@@ -1,10 +1,10 @@
import 'package:fl_clash/enum/enum.dart';
import 'dart:math';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:provider/provider.dart';
import 'network_detection.dart';
import 'outbound_mode.dart';
import 'start_button.dart';
@@ -29,34 +29,35 @@ class _DashboardFragmentState extends State<DashboardFragment> {
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Selector<AppState, ViewMode>(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, ___) {
final isDesktop = viewMode == ViewMode.desktop;
child: Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, ___) {
// final viewMode = other.getViewMode(viewWidth);
// final isDesktop = viewMode == ViewMode.desktop;
return Grid(
crossAxisCount: 12,
crossAxisCount: max(4 * ((viewWidth / 320).ceil()), 8),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
children: const [
GridItem(
crossAxisCellCount: isDesktop ? 8 : 12,
child: const NetworkSpeed(),
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const OutboundMode(),
crossAxisCellCount: 4,
child: OutboundMode(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const NetworkDetection(),
crossAxisCellCount: 4,
child: NetworkDetection(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const TrafficUsage(),
crossAxisCellCount: 4,
child: TrafficUsage(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const IntranetIP(),
crossAxisCellCount: 4,
child: IntranetIP(),
),
],
);

View File

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

View File

@@ -72,9 +72,6 @@ class _LogsFragmentState extends State<LogsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
});
}

View File

@@ -72,17 +72,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
},
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
)
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
},
);
}
@@ -113,74 +103,82 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
@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,
return FloatLayout(
floatingWidget: FloatWrapper(
child: FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
),
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
profileItemKeys = state.profiles
.map((profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
},
);
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 56 : 0),
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: _changeProfile,
),
),
],
),
);
},
),
),
);
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
profileItemKeys = state.profiles
.map(
(profile) => GlobalObjectKey<_ProfileItemState>(profile.id))
.toList();
final columns = _getColumns(state.viewMode);
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
hasPadding.value =
scrollNotification.metrics.maxScrollExtent > 0;
return true;
},
child: ValueListenableBuilder(
valueListenable: hasPadding,
builder: (_, hasPadding, __) {
return SingleChildScrollView(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + (hasPadding ? 72 : 0),
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: profileItemKeys[i],
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: _changeProfile,
),
),
],
),
);
},
),
),
);
},
),
),
);
}
@@ -267,11 +265,13 @@ class _ProfileItemState extends State<ProfileItem> {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
profile.label ?? profile.id,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
Flexible(
child: Text(
profile.label ?? profile.id,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',

View File

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

View File

@@ -0,0 +1,192 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProxyCard extends StatelessWidget {
final String groupName;
final Proxy proxy;
final bool isSelected;
final CommonCardType style;
final ProxyCardType type;
const ProxyCard({
super.key,
required this.groupName,
required this.proxy,
required this.isSelected,
this.style = CommonCardType.plain,
required this.type,
});
Measure get measure => globalState.appController.measure;
Widget _buildDelayText() {
return SizedBox(
height: measure.labelSmallHeight,
child: Selector<AppState, int?>(
selector: (context, appState) => appState.getDelay(
proxy.name,
),
builder: (context, delay, __) {
return FadeBox(
child: Builder(
builder: (_) {
if (delay == null) {
return Container();
}
if (delay == 0) {
return SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: const CircularProgressIndicator(
strokeWidth: 2,
),
);
}
return Text(
delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: other.getDelayColor(
delay,
),
),
);
},
),
);
},
),
);
}
Widget _buildProxyNameText(BuildContext context) {
if (type == ProxyCardType.min) {
return SizedBox(
height: measure.bodyMediumHeight * 1,
child: Text(
proxy.name,
maxLines: 1,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
),
);
} else {
return SizedBox(
height: measure.bodyMediumHeight * 2,
child: Text(
proxy.name,
maxLines: 2,
style: context.textTheme.bodyMedium?.copyWith(
overflow: TextOverflow.ellipsis,
),
),
);
}
}
_changeProxy(BuildContext context) {
final appController = globalState.appController;
final group = appController.appState.getGroupWithName(groupName)!;
if (group.type != GroupType.Selector) {
globalState.showSnackBar(
context,
message: appLocalizations.notSelectedTip,
);
return;
}
globalState.appController.config.updateCurrentSelectedMap(
groupName,
proxy.name,
);
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxy.name,
),
);
}
@override
Widget build(BuildContext context) {
final measure = globalState.appController.measure;
final delayText = _buildDelayText();
final proxyNameText = _buildProxyNameText(context);
return CommonCard(
type: style,
key: key,
onPressed: () {
_changeProxy(context);
},
isSelected: isSelected,
child: Container(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
proxyNameText,
const SizedBox(
height: 8,
),
if (type == ProxyCardType.expand) ...[
SizedBox(
height: measure.bodySmallHeight,
child: Selector<AppState, String>(
selector: (context, appState) => appState.getDesc(
proxy.type,
proxy.name,
),
builder: (_, desc, __) {
return TooltipText(
text: Text(
desc,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color?.toLight(),
),
),
);
},
),
),
const SizedBox(
height: 8,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color:
context.textTheme.bodySmall?.color?.toLight(),
),
),
),
),
delayText,
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,404 @@
import 'dart:math';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'card.dart';
class ProxyGroupView extends StatefulWidget {
final String groupName;
final ProxiesType type;
const ProxyGroupView({
super.key,
required this.groupName,
required this.type,
});
@override
State<ProxyGroupView> createState() => _ProxyGroupViewState();
}
class _ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final scrollController = ScrollController();
var isEnd = false;
String get groupName => widget.groupName;
ProxiesType get type => widget.type;
double _getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.appController.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
return switch(proxyCardType){
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
ProxyCardType.shrink => baseHeight,
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
};
}
_delayTest(List<Proxy> proxies) async {
if (isLock) return;
isLock = true;
final appController = globalState.appController;
for (final proxy in proxies) {
final proxyName =
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(
name: proxyName,
value: 0,
),
);
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
appController.appState.sortNum++;
isLock = false;
}
Widget _currentProxyNameBuilder({
required Widget Function(String) builder,
}) {
return Selector2<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return config.currentSelectedMap[groupName] ?? group.now ?? '';
},
builder: (_, value, ___) {
return builder(value);
},
);
}
Widget _buildTabGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
child: Align(
alignment: Alignment.topCenter,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(builder: (value) {
return ProxyCard(
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
isSelected: value == proxy.name,
proxy: proxy,
groupName: groupName,
);
});
},
),
),
);
}
Widget _buildExpansionGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
final itemHeight = _getItemHeight(proxyCardType);
final innerHeight = context.appSize.height - 200;
final lines = (sortedProxies.length / columns).ceil();
final minLines =
innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3;
final height = (itemHeight + 8) * min(lines, minLines) - 8;
return Selector<Config, Set<String>>(
selector: (_, config) => config.currentUnfoldSet,
builder: (_, currentUnfoldSet, __) {
return CommonCard(
child: ExpansionTile(
childrenPadding: const EdgeInsets.all(8),
initiallyExpanded: currentUnfoldSet.contains(groupName),
iconColor: context.colorScheme.onSurfaceVariant,
onExpansionChanged: (value) {
final tempUnfoldSet = Set<String>.from(currentUnfoldSet);
if (value) {
tempUnfoldSet.add(groupName);
} else {
tempUnfoldSet.remove(groupName);
}
globalState.appController.config.updateCurrentUnfoldSet(
tempUnfoldSet,
);
},
controlAffinity: ListTileControlAffinity.trailing,
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(groupName),
const SizedBox(
height: 4,
),
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
group.type.name,
style: context.textTheme.labelMedium?.toLight,
),
Flexible(
flex: 1,
child: _currentProxyNameBuilder(
builder: (value) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
if (value.isNotEmpty) ...[
Icon(
Icons.arrow_right,
color: context
.colorScheme.onSurfaceVariant,
),
Flexible(
flex: 1,
child: Text(
overflow: TextOverflow.ellipsis,
value,
style: context
.textTheme.labelMedium?.toLight,
),
),
]
],
);
},
),
),
],
),
),
const SizedBox(
height: 4,
),
],
),
),
IconButton(
icon: Icon(
Icons.network_ping,
size: 20,
color: context.colorScheme.onSurfaceVariant,
),
onPressed: () {
_delayTest(sortedProxies);
},
),
],
),
shape: const RoundedRectangleBorder(
side: BorderSide.none,
),
collapsedShape: const RoundedRectangleBorder(
side: BorderSide.none,
),
children: [
SizedBox(
height: height,
child: GridView.builder(
key: widget.key,
controller: scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(
builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
);
},
),
),
],
),
);
},
);
}
@override
void dispose() {
super.dispose();
scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
sortNum: appState.sortNum,
proxies: group.all,
);
},
builder: (_, state, __) {
final proxies = state.proxies;
final columns = state.columns;
final proxyCardType = state.proxyCardType;
return switch (type) {
ProxiesType.tab => _buildTabGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
),
ProxiesType.list => _buildExpansionGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
),
};
},
);
}
}
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 200,
),
);
_scale = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
),
),
child: widget.child,
);
}
}

View File

@@ -0,0 +1,50 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'group.dart';
class ProxiesListFragment extends StatefulWidget {
const ProxiesListFragment({super.key});
@override
State<ProxiesListFragment> createState() =>
_ProxiesListFragmentState();
}
class _ProxiesListFragmentState
extends State<ProxiesListFragment> {
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: state.groupNames.length,
itemBuilder: (_, index) {
final groupName = state.groupNames[index];
return ProxyGroupView(
key: PageStorageKey(groupName),
groupName: groupName,
type: ProxiesType.list,
);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 16,
);
},
);
},
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/list.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'setting.dart';
import 'tab.dart';
class ProxiesFragment extends StatefulWidget {
const ProxiesFragment({super.key});
@override
State<ProxiesFragment> createState() => _ProxiesFragmentState();
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
builder: (context) {
return const ProxiesSettingWidget();
},
);
},
icon: const Icon(
Icons.tune,
),
)
];
});
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
},
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return switch (proxiesType) {
ProxiesType.tab => const ProxiesTabFragment(),
ProxiesType.list => const ProxiesListFragment(),
};
},
),
);
}
}

View File

@@ -0,0 +1,262 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProxiesSettingWidget extends StatelessWidget {
const ProxiesSettingWidget({super.key});
IconData _getIconWithProxiesType(ProxiesType type) {
return switch (type) {
ProxiesType.tab => Icons.view_carousel,
ProxiesType.list => Icons.view_list,
};
}
IconData _getIconWithProxiesSortType(ProxiesSortType type) {
return switch (type) {
ProxiesSortType.none => Icons.sort,
ProxiesSortType.delay => Icons.network_ping,
ProxiesSortType.name => Icons.sort_by_alpha,
};
}
String _getStringProxiesSortType(ProxiesSortType type) {
return switch (type) {
ProxiesSortType.none => appLocalizations.defaultText,
ProxiesSortType.delay => appLocalizations.delay,
ProxiesSortType.name => appLocalizations.name,
};
}
List<Widget> _buildStyleSetting() {
return generateSection(
title: appLocalizations.style,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
final config = globalState.appController.config;
return Wrap(
spacing: 16,
children: [
for (final item in ProxiesType.values)
SettingInfoCard(
Info(
label: Intl.message(item.name),
iconData: _getIconWithProxiesType(item),
),
isSelected: proxiesType == item,
onPressed: () {
config.proxiesType = item;
},
)
],
);
},
),
)
],
);
}
List<Widget> _buildSortSetting() {
return generateSection(
title: appLocalizations.sort,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesSortType,
builder: (_, proxiesSortType, __) {
final config = globalState.appController.config;
return Wrap(
spacing: 16,
children: [
for (final item in ProxiesSortType.values)
SettingInfoCard(
Info(
label: _getStringProxiesSortType(item),
iconData: _getIconWithProxiesSortType(item),
),
isSelected: proxiesSortType == item,
onPressed: () {
config.proxiesSortType = item;
},
),
],
);
},
),
),
],
);
}
List<Widget> _buildSizeSetting() {
return generateSection(
title: appLocalizations.size,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxyCardType>(
selector: (_, config) => config.proxyCardType,
builder: (_, proxyCardType, __) {
final config = globalState.appController.config;
return Wrap(
spacing: 16,
children: [
for (final item in ProxyCardType.values)
SettingTextCard(
Intl.message(item.name),
isSelected: item == proxyCardType,
onPressed: () {
config.proxyCardType = item;
},
)
],
);
},
),
)
],
);
}
List<Widget> _buildColumnsSetting() {
return generateSection(
title: appLocalizations.columns,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector2<AppState, Config, ColumnsSelectorState>(
selector: (_, appState, config) => ColumnsSelectorState(
columns: config.proxiesColumns,
viewMode: appState.viewMode,
),
builder: (_, state, __) {
final config = globalState.appController.config;
final targetColumnsArray = viewModeColumnsMap[state.viewMode]!;
final currentColumns = other.getColumns(
state.viewMode,
state.columns,
);
return Wrap(
spacing: 16,
children: [
for (final item in targetColumnsArray)
SettingTextCard(
other.getColumnsTextForInt(item),
isSelected: item == currentColumns,
onPressed: () {
config.proxiesColumns = item;
},
)
],
);
},
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildStyleSetting(),
..._buildSortSetting(),
..._buildColumnsSetting(),
..._buildSizeSetting(),
],
),
);
}
}
class SettingInfoCard extends StatelessWidget {
final Info info;
final bool? isSelected;
final VoidCallback onPressed;
const SettingInfoCard(
this.info, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
isSelected: isSelected,
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Icon(info.iconData),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
info.label,
style: context.textTheme.bodyMedium,
),
),
],
),
),
);
}
}
class SettingTextCard extends StatelessWidget {
final String text;
final bool? isSelected;
final VoidCallback onPressed;
const SettingTextCard(
this.text, {
super.key,
this.isSelected,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: onPressed,
isSelected: isSelected,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
text,
style: context.textTheme.bodyMedium,
),
),
);
}
}

View File

@@ -0,0 +1,219 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/setting.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'group.dart';
class ProxiesTabFragment extends StatefulWidget {
const ProxiesTabFragment({super.key});
@override
State<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
}
class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
with TickerProviderStateMixin {
TabController? _tabController;
final hasMoreButtonNotifier = ValueNotifier<bool>(false);
@override
void dispose() {
super.dispose();
_tabController?.dispose();
}
_buildMoreButton() {
return Selector<AppState, bool>(
selector: (_, appState) => appState.viewMode == ViewMode.mobile,
builder: (_, value, ___) {
return IconButton(
onPressed: _showMoreMenu,
icon: value
? const Icon(
Icons.expand_more,
)
: const Icon(
Icons.chevron_right,
),
);
},
);
}
_showMoreMenu() {
showSheet(
context: context,
width: 380,
isScrollControlled: false,
builder: (context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return SizedBox(
width: double.infinity,
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
spacing: 8,
children: [
for (final groupName in state.groupNames)
SettingTextCard(
groupName,
onPressed: () {
final index = state.groupNames
.indexWhere((item) => item == groupName);
if (index == -1) return;
_tabController?.animateTo(index);
globalState.appController.config
.updateCurrentGroupName(
groupName,
);
},
isSelected: groupName == state.currentGroupName,
)
],
),
);
},
),
);
},
title: appLocalizations.proxyGroup,
);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_tabController?.dispose();
_tabController = null;
return true;
}
return false;
},
builder: (_, state, __) {
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NotificationListener<ScrollMetricsNotification>(
onNotification: (scrollNotification) {
hasMoreButtonNotifier.value =
scrollNotification.metrics.maxScrollExtent > 0;
return true;
},
child: ValueListenableBuilder(
valueListenable: hasMoreButtonNotifier,
builder: (_, value, child) {
return Stack(
alignment: AlignmentDirectional.centerStart,
children: [
TabBar(
controller: _tabController,
padding: EdgeInsets.only(
left: 16,
right: 16 + (value ? 16 : 0),
),
onTap: (index) {
final appController = globalState.appController;
final currentGroups =
appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(
currentGroups[index].name,
);
}
},
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
),
],
),
if (value)
Positioned(
right: 0,
child: child!,
),
],
);
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
context.colorScheme.surface.withOpacity(0.1),
context.colorScheme.surface,
],
stops: const [
0.0,
0.1
]),
),
child: _buildMoreButton(),
),
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxyGroupView(
groupName: groupName,
type: ProxiesType.tab,
),
),
],
),
)
],
);
},
);
}
}

View File

@@ -1,11 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
@@ -68,9 +66,6 @@ class _RequestsFragmentState extends State<RequestsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
)
];
},
);
@@ -156,7 +151,7 @@ class _RequestsFragmentState extends State<RequestsFragment> {
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return RequestItem(
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
@@ -178,104 +173,6 @@ class _RequestsFragmentState extends State<RequestsFragment> {
}
}
class RequestItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
const RequestItem({
super.key,
required this.connection,
this.onClick,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: Platform.isAndroid
? Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
);
}
}
class RequestsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> requestsNotifier;
@@ -394,7 +291,7 @@ class RequestsSearchDelegate extends SearchDelegate {
child: ListView.separated(
itemBuilder: (_, index) {
final connection = _results[index];
return RequestItem(
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {

View File

@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -21,8 +23,95 @@ class ThemeModeItem {
class ThemeFragment extends StatelessWidget {
const ThemeFragment({super.key});
Widget _themeModeCheckBox({
Widget _itemCard({
required BuildContext context,
required Info info,
required Widget child,
}) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
),
child: Wrap(
runSpacing: 16,
children: [
InfoHeader(
info: info,
),
child,
],
),
);
}
@override
Widget build(BuildContext context) {
final previewCard = Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CommonCard(
onPressed: (){
},
info: Info(
label: appLocalizations.preview,
iconData: Icons.looks,
),
child: Container(
height: 200,
),
),
);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
previewCard,
const ThemeColorsBox(),
],
),
);
}
}
class ItemCard extends StatelessWidget {
final Widget child;
final Info info;
const ItemCard({
super.key,
required this.info,
required this.child,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
),
child: Wrap(
runSpacing: 16,
children: [
InfoHeader(
info: info,
),
child,
],
),
);
}
}
class ThemeColorsBox extends StatefulWidget {
const ThemeColorsBox({super.key});
@override
State<ThemeColorsBox> createState() => _ThemeColorsBoxState();
}
class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Widget _themeModeCheckBox({
bool? isSelected,
required ThemeModeItem themeModeItem,
}) {
@@ -32,7 +121,7 @@ class ThemeFragment extends StatelessWidget {
globalState.appController.config.themeMode = themeModeItem.themeMode;
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal:16),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
@@ -55,7 +144,6 @@ class ThemeFragment extends StatelessWidget {
}
Widget _primaryColorCheckBox({
required BuildContext context,
bool? isSelected,
Color? color,
}) {
@@ -68,28 +156,8 @@ class ThemeFragment extends StatelessWidget {
);
}
Widget _itemCard({
required BuildContext context,
required Info info,
required Widget child,
}) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
),
child: Wrap(
runSpacing: 16,
children: [
InfoHeader(
info: info,
),
child,
],
),
);
}
Widget _getThemeCard(BuildContext context) {
@override
Widget build(BuildContext context) {
List<ThemeModeItem> themeModeItems = [
ThemeModeItem(
iconData: Icons.auto_mode,
@@ -118,8 +186,7 @@ class ThemeFragment extends StatelessWidget {
];
return Column(
children: [
_itemCard(
context: context,
ItemCard(
info: Info(
label: appLocalizations.themeMode,
iconData: Icons.brightness_high,
@@ -137,7 +204,6 @@ class ThemeFragment extends StatelessWidget {
final themeModeItem = themeModeItems[index];
return _themeModeCheckBox(
isSelected: themeMode == themeModeItem.themeMode,
context: context,
themeModeItem: themeModeItem,
);
},
@@ -151,8 +217,7 @@ class ThemeFragment extends StatelessWidget {
},
),
),
_itemCard(
context: context,
ItemCard(
info: Info(
label: appLocalizations.themeColor,
iconData: Icons.palette,
@@ -172,7 +237,6 @@ class ThemeFragment extends StatelessWidget {
itemBuilder: (_, index) {
final primaryColor = primaryColors[index];
return _primaryColorCheckBox(
context: context,
isSelected: currentPrimaryColor == primaryColor?.value,
color: primaryColor,
);
@@ -191,30 +255,4 @@ class ThemeFragment extends StatelessWidget {
],
);
}
@override
Widget build(BuildContext context) {
final themeCard = _getThemeCard(context);
final previewCard = Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CommonCard(
info: Info(
label: appLocalizations.preview,
iconData: Icons.looks,
),
child: Container(
height: 200,
),
),
);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
previewCard,
themeCard,
],
),
);
}
}

View File

@@ -177,14 +177,14 @@
"geodataLoader": "Geo Low Memory Mode",
"geodataLoaderDesc": "Enabling will use the Geo low memory loader",
"requests": "Requests",
"requestsDesc": "View recently requested data",
"requestsDesc": "View recently request records",
"findProcessMode": "Find process",
"findProcessModeDesc": "There is a risk of flashback after opening",
"init": "Init",
"infiniteTime": "Long term effective",
"expirationTime": "Expiration time",
"connections": "Connections",
"connectionsDesc": "View current connection",
"connectionsDesc": "View current connections data",
"nullRequestsDesc": "No requests",
"nullConnectionsDesc": "No connections",
"intranetIP": "Intranet IP",
@@ -195,5 +195,21 @@
"testUrl": "Test url",
"sync": "Sync",
"exclude": "Hidden from recent tasks",
"excludeDesc": "When the app is in the background, the app is hidden from the recent task"
"excludeDesc": "When the app is in the background, the app is hidden from the recent task",
"oneColumn": "One column",
"twoColumns": "Two columns",
"threeColumns": "Three columns",
"fourColumns": "Four columns",
"expand": "Standard",
"shrink": "Shrink",
"min": "Min",
"tab": "Tab",
"list": "List",
"delay": "Delay",
"style": "Style",
"size": "Size",
"sort": "Sort",
"columns": "Columns",
"proxiesSetting": "Proxies setting",
"proxyGroup": "Proxy group"
}

View File

@@ -71,7 +71,7 @@
"qrcode": "二维码",
"qrcodeDesc": "扫描二维码获取配置文件",
"url": "URL",
"urlDesc": "直接上传配置文件",
"urlDesc": "通过URL获取配置文件",
"file": "文件",
"fileDesc": "直接上传配置文件",
"name": "名称",
@@ -177,14 +177,14 @@
"geodataLoader": "Geo低内存模式",
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求",
"requestsDesc": "查看最近请求数据",
"requestsDesc": "查看最近请求记录",
"findProcessMode": "查找进程",
"findProcessModeDesc": "开启后存在闪退风险",
"init": "初始化",
"infiniteTime": "长期有效",
"expirationTime": "到期时间",
"connections": "连接",
"connectionsDesc": "查看当前连接",
"connectionsDesc": "查看当前连接数据",
"nullRequestsDesc": "暂无请求",
"nullConnectionsDesc": "暂无连接",
"intranetIP": "内网 IP",
@@ -195,5 +195,21 @@
"testUrl": "测速链接",
"sync": "同步",
"exclude": "从最近任务中隐藏",
"excludeDesc": "应用在后台时,从最近任务中隐藏应用"
"excludeDesc": "应用在后台时,从最近任务中隐藏应用",
"oneColumn": "一列",
"twoColumns": "两列",
"threeColumns": "三列",
"fourColumns": "四列",
"expand": "标准",
"shrink": "紧凑",
"min": "最小",
"tab": "标签页",
"list": "列表",
"delay": "延迟",
"style": "风格",
"size": "尺寸",
"sort": "排序",
"columns": "列数",
"proxiesSetting": "代理设置",
"proxyGroup": "代理组"
}

View File

@@ -87,14 +87,15 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
"The current application is already the latest version"),
"checking": MessageLookupByLibrary.simpleMessage("Checking..."),
"columns": MessageLookupByLibrary.simpleMessage("Columns"),
"compatible":
MessageLookupByLibrary.simpleMessage("Compatibility mode"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
"Opening it will lose part of its application ability and gain the support of full amount of Clash."),
"confirm": MessageLookupByLibrary.simpleMessage("Confirm"),
"connections": MessageLookupByLibrary.simpleMessage("Connections"),
"connectionsDesc":
MessageLookupByLibrary.simpleMessage("View current connection"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage(
"View current connections data"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
@@ -107,6 +108,7 @@ class MessageLookup extends MessageLookupByLibrary {
"days": MessageLookupByLibrary.simpleMessage("Days"),
"defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"),
"defaultText": MessageLookupByLibrary.simpleMessage("Default"),
"delay": MessageLookupByLibrary.simpleMessage("Delay"),
"delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"),
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
"desc": MessageLookupByLibrary.simpleMessage(
@@ -126,6 +128,7 @@ class MessageLookup extends MessageLookupByLibrary {
"excludeDesc": MessageLookupByLibrary.simpleMessage(
"When the app is in the background, the app is hidden from the recent task"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
"expirationTime":
MessageLookupByLibrary.simpleMessage("Expiration time"),
"externalController":
@@ -142,6 +145,7 @@ class MessageLookup extends MessageLookupByLibrary {
"findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage(
"There is a risk of flashback after opening"),
"fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"),
"general": MessageLookupByLibrary.simpleMessage("General"),
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
"geodataLoader":
@@ -162,12 +166,14 @@ class MessageLookup extends MessageLookupByLibrary {
"just": MessageLookupByLibrary.simpleMessage("Just"),
"language": MessageLookupByLibrary.simpleMessage("Language"),
"light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"),
"logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"),
"logcat": MessageLookupByLibrary.simpleMessage("Logcat"),
"logcatDesc": MessageLookupByLibrary.simpleMessage(
"Disabling will hide the log entry"),
"logs": MessageLookupByLibrary.simpleMessage("Logs"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"),
"min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
@@ -195,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary {
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"No profile, Please add a profile"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
"other": MessageLookupByLibrary.simpleMessage("Other"),
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
"override": MessageLookupByLibrary.simpleMessage("Override"),
@@ -230,6 +237,9 @@ class MessageLookup extends MessageLookupByLibrary {
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"project": MessageLookupByLibrary.simpleMessage("Project"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
"proxiesSetting":
MessageLookupByLibrary.simpleMessage("Proxies setting"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"),
"proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Set the Clash listening port"),
@@ -247,7 +257,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Recovery success"),
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
"requestsDesc": MessageLookupByLibrary.simpleMessage(
"View recently requested data"),
"View recently request records"),
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
"External resource related info"),
@@ -258,16 +268,21 @@ class MessageLookup extends MessageLookupByLibrary {
"selected": MessageLookupByLibrary.simpleMessage("Selected"),
"settings": MessageLookupByLibrary.simpleMessage("Settings"),
"show": MessageLookupByLibrary.simpleMessage("Show"),
"shrink": MessageLookupByLibrary.simpleMessage("Shrink"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("SilentLaunch"),
"silentLaunchDesc":
MessageLookupByLibrary.simpleMessage("Start in the background"),
"size": MessageLookupByLibrary.simpleMessage("Size"),
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"tab": MessageLookupByLibrary.simpleMessage("Tab"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"When enabled, the home tab will add a toggle animation"),
@@ -280,12 +295,14 @@ class MessageLookup extends MessageLookupByLibrary {
"themeDesc": MessageLookupByLibrary.simpleMessage(
"Set dark mode,adjust the color"),
"themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"),
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN mode"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"only effective in administrator mode"),
"twoColumns": MessageLookupByLibrary.simpleMessage("Two columns"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage(
"unable to update current profile"),

View File

@@ -71,12 +71,13 @@ class MessageLookup extends MessageLookupByLibrary {
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc":
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
@@ -89,6 +90,7 @@ class MessageLookup extends MessageLookupByLibrary {
"days": MessageLookupByLibrary.simpleMessage(""),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"),
"desc": MessageLookupByLibrary.simpleMessage(
@@ -104,6 +106,7 @@ class MessageLookup extends MessageLookupByLibrary {
"excludeDesc":
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"expand": MessageLookupByLibrary.simpleMessage("标准"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
@@ -115,6 +118,7 @@ class MessageLookup extends MessageLookupByLibrary {
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"findProcessModeDesc":
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"general": MessageLookupByLibrary.simpleMessage("基础"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
@@ -131,11 +135,13 @@ class MessageLookup extends MessageLookupByLibrary {
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
"language": MessageLookupByLibrary.simpleMessage("语言"),
"light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logs": MessageLookupByLibrary.simpleMessage("日志"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc":
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
@@ -158,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary {
"nullProfileDesc":
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"other": MessageLookupByLibrary.simpleMessage("其他"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"),
@@ -187,6 +194,8 @@ class MessageLookup extends MessageLookupByLibrary {
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
"project": MessageLookupByLibrary.simpleMessage("项目"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
@@ -197,7 +206,7 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求数据"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
@@ -207,15 +216,20 @@ class MessageLookup extends MessageLookupByLibrary {
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
"settings": MessageLookupByLibrary.simpleMessage("设置"),
"show": MessageLookupByLibrary.simpleMessage("显示"),
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
"sort": MessageLookupByLibrary.simpleMessage("排序"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc":
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
@@ -226,11 +240,13 @@ class MessageLookup extends MessageLookupByLibrary {
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("TUN模式"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
@@ -239,7 +255,7 @@ class MessageLookup extends MessageLookupByLibrary {
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
"view": MessageLookupByLibrary.simpleMessage("查看"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),

View File

@@ -1830,10 +1830,10 @@ class AppLocalizations {
);
}
/// `View recently requested data`
/// `View recently request records`
String get requestsDesc {
return Intl.message(
'View recently requested data',
'View recently request records',
name: 'requestsDesc',
desc: '',
args: [],
@@ -1900,10 +1900,10 @@ class AppLocalizations {
);
}
/// `View current connection`
/// `View current connections data`
String get connectionsDesc {
return Intl.message(
'View current connection',
'View current connections data',
name: 'connectionsDesc',
desc: '',
args: [],
@@ -2019,6 +2019,166 @@ class AppLocalizations {
args: [],
);
}
/// `One column`
String get oneColumn {
return Intl.message(
'One column',
name: 'oneColumn',
desc: '',
args: [],
);
}
/// `Two columns`
String get twoColumns {
return Intl.message(
'Two columns',
name: 'twoColumns',
desc: '',
args: [],
);
}
/// `Three columns`
String get threeColumns {
return Intl.message(
'Three columns',
name: 'threeColumns',
desc: '',
args: [],
);
}
/// `Four columns`
String get fourColumns {
return Intl.message(
'Four columns',
name: 'fourColumns',
desc: '',
args: [],
);
}
/// `Standard`
String get expand {
return Intl.message(
'Standard',
name: 'expand',
desc: '',
args: [],
);
}
/// `Shrink`
String get shrink {
return Intl.message(
'Shrink',
name: 'shrink',
desc: '',
args: [],
);
}
/// `Min`
String get min {
return Intl.message(
'Min',
name: 'min',
desc: '',
args: [],
);
}
/// `Tab`
String get tab {
return Intl.message(
'Tab',
name: 'tab',
desc: '',
args: [],
);
}
/// `List`
String get list {
return Intl.message(
'List',
name: 'list',
desc: '',
args: [],
);
}
/// `Delay`
String get delay {
return Intl.message(
'Delay',
name: 'delay',
desc: '',
args: [],
);
}
/// `Style`
String get style {
return Intl.message(
'Style',
name: 'style',
desc: '',
args: [],
);
}
/// `Size`
String get size {
return Intl.message(
'Size',
name: 'size',
desc: '',
args: [],
);
}
/// `Sort`
String get sort {
return Intl.message(
'Sort',
name: 'sort',
desc: '',
args: [],
);
}
/// `Columns`
String get columns {
return Intl.message(
'Columns',
name: 'columns',
desc: '',
args: [],
);
}
/// `Proxies setting`
String get proxiesSetting {
return Intl.message(
'Proxies setting',
name: 'proxiesSetting',
desc: '',
args: [],
);
}
/// `Proxy group`
String get proxyGroup {
return Intl.message(
'Proxy group',
name: 'proxyGroup',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -15,12 +15,12 @@ import 'common/common.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await android?.init();
await window?.init();
clashCore.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init();
await window?.init(config.windowProps);
final appState = AppState(
mode: clashConfig.mode,
isCompatible: config.isCompatible,
@@ -67,7 +67,6 @@ Future<void> vpnService() async {
clashCore.setFdMap(fd.id);
},
onProcess: (Process process) async {
print(process);
var packageName = await app?.resolverProcess(process);
clashCore.setProcessMap(
ProcessMapItem(
@@ -112,6 +111,7 @@ Future<void> vpnService() async {
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.stopSystemProxy();
clashCore.shutdown();
exit(0);
},
),
@@ -119,7 +119,7 @@ Future<void> vpnService() async {
globalState.updateTraffic();
globalState.updateFunctionLists = [
() {
() {
globalState.updateTraffic();
}
];
@@ -137,7 +137,8 @@ class ServiceMessageHandler with ServiceMessageListener {
required Function(Process process) onProcess,
required Function(String runTime) onStarted,
required Function(String groupName) onLoaded,
}) : _onProtect = onProtect,
})
: _onProtect = onProtect,
_onProcess = onProcess,
_onStarted = onStarted,
_onLoaded = onLoaded;
@@ -163,10 +164,11 @@ class ServiceMessageHandler with ServiceMessageListener {
}
}
@immutable
class TileListenerWithVpn with TileListener {
final Function() _onStop;
TileListenerWithVpn({
const TileListenerWithVpn({
required Function() onStop,
}) : _onStop = onStop;

View File

@@ -311,6 +311,13 @@ class ClashConfig extends ChangeNotifier {
_mode = clashConfig._mode;
_logLevel = clashConfig._logLevel;
_tun = clashConfig._tun;
_findProcessMode = clashConfig._findProcessMode;
_geoXUrl = clashConfig._geoXUrl;
_unifiedDelay = clashConfig._unifiedDelay;
_globalRealUa = clashConfig._globalRealUa;
_tcpConcurrent = clashConfig._tcpConcurrent;
_externalController = clashConfig._externalController;
_geodataLoader = clashConfig._geodataLoader;
_dns = clashConfig._dns;
_rules = clashConfig._rules;
_globalRealUa = clashConfig.globalRealUa;

View File

@@ -29,13 +29,28 @@ class AccessControl with _$AccessControl {
class Props with _$Props {
const factory Props({
AccessControl? accessControl,
bool? allowBypass,
bool? systemProxy,
required bool allowBypass,
required bool systemProxy,
}) = _Props;
factory Props.fromJson(Map<String, Object?> json) => _$PropsFromJson(json);
}
@freezed
class WindowProps with _$WindowProps {
const factory WindowProps({
@Default(1000) double width,
@Default(600) double height,
double? top,
double? left,
}) = _WindowProps;
factory WindowProps.fromJson(Map<String, Object?>? json) =>
json == null ? defaultWindowProps : _$WindowPropsFromJson(json);
}
const defaultWindowProps = WindowProps();
@JsonSerializable()
class Config extends ChangeNotifier {
List<Profile> _profiles;
@@ -62,6 +77,7 @@ class Config extends ChangeNotifier {
ProxyCardType _proxyCardType;
int _proxiesColumns;
String _testUrl;
WindowProps _windowProps;
Config()
: _profiles = [],
@@ -76,13 +92,14 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true,
_isAccessControl = false,
_autoCheckUpdate = true,
_systemProxy = true,
_systemProxy = false,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
_allowBypass = true,
_isExclude = false,
_proxyCardType = ProxyCardType.expand,
_windowProps = defaultWindowProps,
_proxiesType = ProxiesType.tab,
_proxiesColumns = 2;
@@ -376,7 +393,7 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: true)
@JsonKey(defaultValue: false)
bool get systemProxy {
return _systemProxy;
}
@@ -388,7 +405,10 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: ProxiesType.tab)
@JsonKey(
defaultValue: ProxiesType.tab,
unknownEnumValue: ProxiesType.tab,
)
ProxiesType get proxiesType => _proxiesType;
set proxiesType(ProxiesType value) {
@@ -438,6 +458,15 @@ class Config extends ChangeNotifier {
}
}
WindowProps get windowProps => _windowProps;
set windowProps(WindowProps value) {
if (_windowProps != value) {
_windowProps = value;
notifyListeners();
}
}
update([
Config? config,
RecoveryOption recoveryOptions = RecoveryOption.all,
@@ -469,8 +498,9 @@ class Config extends ChangeNotifier {
_accessControl = config._accessControl;
_isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate;
_dav = config._dav;
_testUrl = config.testUrl;
_testUrl = config._testUrl;
_isExclude = config._isExclude;
_windowProps = config._windowProps;
}
notifyListeners();
}
@@ -482,9 +512,4 @@ class Config extends ChangeNotifier {
factory Config.fromJson(Map<String, dynamic> json) {
return _$ConfigFromJson(json);
}
@override
String toString() {
return 'Config{_profiles: $_profiles, _isCompatible: $_isCompatible, _currentProfileId: $_currentProfileId, _autoLaunch: $_autoLaunch, _silentLaunch: $_silentLaunch, _autoRun: $_autoRun, _openLog: $_openLog, _themeMode: $_themeMode, _locale: $_locale, _primaryColor: $_primaryColor, _proxiesSortType: $_proxiesSortType, _isMinimizeOnExit: $_isMinimizeOnExit, _isAccessControl: $_isAccessControl, _accessControl: $_accessControl, _isAnimateToPage: $_isAnimateToPage, _dav: $_dav}';
}
}

View File

@@ -48,7 +48,11 @@ class ConnectionsAndKeywords with _$ConnectionsAndKeywords {
_$ConnectionsAndKeywordsFromJson(json);
}
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords{
List<Connection> get filteredConnections => connections.where((connection)=> Set.from(connection.chains).containsAll(keywords)).toList();
}
extension ConnectionsAndKeywordsExt on ConnectionsAndKeywords {
List<Connection> get filteredConnections => connections
.where((connection) => {
...connection.chains,
connection.metadata.process,
}.containsAll(keywords))
.toList();
}

View File

@@ -247,8 +247,8 @@ Props _$PropsFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$Props {
AccessControl? get accessControl => throw _privateConstructorUsedError;
bool? get allowBypass => throw _privateConstructorUsedError;
bool? get systemProxy => throw _privateConstructorUsedError;
bool get allowBypass => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -260,8 +260,7 @@ abstract class $PropsCopyWith<$Res> {
factory $PropsCopyWith(Props value, $Res Function(Props) then) =
_$PropsCopyWithImpl<$Res, Props>;
@useResult
$Res call(
{AccessControl? accessControl, bool? allowBypass, bool? systemProxy});
$Res call({AccessControl? accessControl, bool allowBypass, bool systemProxy});
$AccessControlCopyWith<$Res>? get accessControl;
}
@@ -280,22 +279,22 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props>
@override
$Res call({
Object? accessControl = freezed,
Object? allowBypass = freezed,
Object? systemProxy = freezed,
Object? allowBypass = null,
Object? systemProxy = null,
}) {
return _then(_value.copyWith(
accessControl: freezed == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
allowBypass: freezed == allowBypass
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool?,
systemProxy: freezed == systemProxy
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool?,
as bool,
) as $Val);
}
@@ -319,8 +318,7 @@ abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> {
__$$PropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AccessControl? accessControl, bool? allowBypass, bool? systemProxy});
$Res call({AccessControl? accessControl, bool allowBypass, bool systemProxy});
@override
$AccessControlCopyWith<$Res>? get accessControl;
@@ -338,22 +336,22 @@ class __$$PropsImplCopyWithImpl<$Res>
@override
$Res call({
Object? accessControl = freezed,
Object? allowBypass = freezed,
Object? systemProxy = freezed,
Object? allowBypass = null,
Object? systemProxy = null,
}) {
return _then(_$PropsImpl(
accessControl: freezed == accessControl
? _value.accessControl
: accessControl // ignore: cast_nullable_to_non_nullable
as AccessControl?,
allowBypass: freezed == allowBypass
allowBypass: null == allowBypass
? _value.allowBypass
: allowBypass // ignore: cast_nullable_to_non_nullable
as bool?,
systemProxy: freezed == systemProxy
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
as bool?,
as bool,
));
}
}
@@ -361,7 +359,10 @@ class __$$PropsImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$PropsImpl implements _Props {
const _$PropsImpl({this.accessControl, this.allowBypass, this.systemProxy});
const _$PropsImpl(
{this.accessControl,
required this.allowBypass,
required this.systemProxy});
factory _$PropsImpl.fromJson(Map<String, dynamic> json) =>
_$$PropsImplFromJson(json);
@@ -369,9 +370,9 @@ class _$PropsImpl implements _Props {
@override
final AccessControl? accessControl;
@override
final bool? allowBypass;
final bool allowBypass;
@override
final bool? systemProxy;
final bool systemProxy;
@override
String toString() {
@@ -413,19 +414,210 @@ class _$PropsImpl implements _Props {
abstract class _Props implements Props {
const factory _Props(
{final AccessControl? accessControl,
final bool? allowBypass,
final bool? systemProxy}) = _$PropsImpl;
required final bool allowBypass,
required final bool systemProxy}) = _$PropsImpl;
factory _Props.fromJson(Map<String, dynamic> json) = _$PropsImpl.fromJson;
@override
AccessControl? get accessControl;
@override
bool? get allowBypass;
bool get allowBypass;
@override
bool? get systemProxy;
bool get systemProxy;
@override
@JsonKey(ignore: true)
_$$PropsImplCopyWith<_$PropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
WindowProps _$WindowPropsFromJson(Map<String, dynamic> json) {
return _WindowProps.fromJson(json);
}
/// @nodoc
mixin _$WindowProps {
double get width => throw _privateConstructorUsedError;
double get height => throw _privateConstructorUsedError;
double? get top => throw _privateConstructorUsedError;
double? get left => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$WindowPropsCopyWith<WindowProps> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $WindowPropsCopyWith<$Res> {
factory $WindowPropsCopyWith(
WindowProps value, $Res Function(WindowProps) then) =
_$WindowPropsCopyWithImpl<$Res, WindowProps>;
@useResult
$Res call({double width, double height, double? top, double? left});
}
/// @nodoc
class _$WindowPropsCopyWithImpl<$Res, $Val extends WindowProps>
implements $WindowPropsCopyWith<$Res> {
_$WindowPropsCopyWithImpl(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? width = null,
Object? height = null,
Object? top = freezed,
Object? left = freezed,
}) {
return _then(_value.copyWith(
width: null == width
? _value.width
: width // ignore: cast_nullable_to_non_nullable
as double,
height: null == height
? _value.height
: height // ignore: cast_nullable_to_non_nullable
as double,
top: freezed == top
? _value.top
: top // ignore: cast_nullable_to_non_nullable
as double?,
left: freezed == left
? _value.left
: left // ignore: cast_nullable_to_non_nullable
as double?,
) as $Val);
}
}
/// @nodoc
abstract class _$$WindowPropsImplCopyWith<$Res>
implements $WindowPropsCopyWith<$Res> {
factory _$$WindowPropsImplCopyWith(
_$WindowPropsImpl value, $Res Function(_$WindowPropsImpl) then) =
__$$WindowPropsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({double width, double height, double? top, double? left});
}
/// @nodoc
class __$$WindowPropsImplCopyWithImpl<$Res>
extends _$WindowPropsCopyWithImpl<$Res, _$WindowPropsImpl>
implements _$$WindowPropsImplCopyWith<$Res> {
__$$WindowPropsImplCopyWithImpl(
_$WindowPropsImpl _value, $Res Function(_$WindowPropsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? width = null,
Object? height = null,
Object? top = freezed,
Object? left = freezed,
}) {
return _then(_$WindowPropsImpl(
width: null == width
? _value.width
: width // ignore: cast_nullable_to_non_nullable
as double,
height: null == height
? _value.height
: height // ignore: cast_nullable_to_non_nullable
as double,
top: freezed == top
? _value.top
: top // ignore: cast_nullable_to_non_nullable
as double?,
left: freezed == left
? _value.left
: left // ignore: cast_nullable_to_non_nullable
as double?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$WindowPropsImpl implements _WindowProps {
const _$WindowPropsImpl(
{this.width = 1000, this.height = 600, this.top, this.left});
factory _$WindowPropsImpl.fromJson(Map<String, dynamic> json) =>
_$$WindowPropsImplFromJson(json);
@override
@JsonKey()
final double width;
@override
@JsonKey()
final double height;
@override
final double? top;
@override
final double? left;
@override
String toString() {
return 'WindowProps(width: $width, height: $height, top: $top, left: $left)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$WindowPropsImpl &&
(identical(other.width, width) || other.width == width) &&
(identical(other.height, height) || other.height == height) &&
(identical(other.top, top) || other.top == top) &&
(identical(other.left, left) || other.left == left));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, width, height, top, left);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith =>
__$$WindowPropsImplCopyWithImpl<_$WindowPropsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$WindowPropsImplToJson(
this,
);
}
}
abstract class _WindowProps implements WindowProps {
const factory _WindowProps(
{final double width,
final double height,
final double? top,
final double? left}) = _$WindowPropsImpl;
factory _WindowProps.fromJson(Map<String, dynamic> json) =
_$WindowPropsImpl.fromJson;
@override
double get width;
@override
double get height;
@override
double? get top;
@override
double? get left;
@override
@JsonKey(ignore: true)
_$$WindowPropsImplCopyWith<_$WindowPropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -35,16 +35,18 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true
..proxiesType =
$enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType']) ??
ProxiesType.tab
..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'],
unknownValue: ProxiesType.tab) ??
ProxiesType.tab
..proxyCardType =
$enumDecodeNullable(_$ProxyCardTypeEnumMap, json['proxyCardType']) ??
ProxyCardType.expand
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
..testUrl =
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
..isExclude = json['isExclude'] as bool? ?? false;
..isExclude = json['isExclude'] as bool? ?? false
..windowProps =
WindowProps.fromJson(json['windowProps'] as Map<String, dynamic>?);
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'profiles': instance.profiles,
@@ -71,6 +73,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'proxiesColumns': instance.proxiesColumns,
'test-url': instance.testUrl,
'isExclude': instance.isExclude,
'windowProps': instance.windowProps,
};
const _$ThemeModeEnumMap = {
@@ -87,12 +90,13 @@ const _$ProxiesSortTypeEnumMap = {
const _$ProxiesTypeEnumMap = {
ProxiesType.tab: 'tab',
ProxiesType.expansion: 'expansion',
ProxiesType.list: 'list',
};
const _$ProxyCardTypeEnumMap = {
ProxyCardType.expand: 'expand',
ProxyCardType.shrink: 'shrink',
ProxyCardType.min: 'min',
};
_$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
@@ -128,8 +132,8 @@ _$PropsImpl _$$PropsImplFromJson(Map<String, dynamic> json) => _$PropsImpl(
? null
: AccessControl.fromJson(
json['accessControl'] as Map<String, dynamic>),
allowBypass: json['allowBypass'] as bool?,
systemProxy: json['systemProxy'] as bool?,
allowBypass: json['allowBypass'] as bool,
systemProxy: json['systemProxy'] as bool,
);
Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
@@ -138,3 +142,19 @@ Map<String, dynamic> _$$PropsImplToJson(_$PropsImpl instance) =>
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
};
_$WindowPropsImpl _$$WindowPropsImplFromJson(Map<String, dynamic> json) =>
_$WindowPropsImpl(
width: (json['width'] as num?)?.toDouble() ?? 1000,
height: (json['height'] as num?)?.toDouble() ?? 600,
top: (json['top'] as num?)?.toDouble(),
left: (json['left'] as num?)?.toDouble(),
);
Map<String, dynamic> _$$WindowPropsImplToJson(_$WindowPropsImpl instance) =>
<String, dynamic>{
'width': instance.width,
'height': instance.height,
'top': instance.top,
'left': instance.left,
};

View File

@@ -2261,3 +2261,143 @@ abstract class _PackageListSelectorState implements PackageListSelectorState {
_$$PackageListSelectorStateImplCopyWith<_$PackageListSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ColumnsSelectorState {
int get columns => throw _privateConstructorUsedError;
ViewMode get viewMode => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ColumnsSelectorStateCopyWith<ColumnsSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ColumnsSelectorStateCopyWith<$Res> {
factory $ColumnsSelectorStateCopyWith(ColumnsSelectorState value,
$Res Function(ColumnsSelectorState) then) =
_$ColumnsSelectorStateCopyWithImpl<$Res, ColumnsSelectorState>;
@useResult
$Res call({int columns, ViewMode viewMode});
}
/// @nodoc
class _$ColumnsSelectorStateCopyWithImpl<$Res,
$Val extends ColumnsSelectorState>
implements $ColumnsSelectorStateCopyWith<$Res> {
_$ColumnsSelectorStateCopyWithImpl(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? columns = null,
Object? viewMode = null,
}) {
return _then(_value.copyWith(
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
) as $Val);
}
}
/// @nodoc
abstract class _$$ColumnsSelectorStateImplCopyWith<$Res>
implements $ColumnsSelectorStateCopyWith<$Res> {
factory _$$ColumnsSelectorStateImplCopyWith(_$ColumnsSelectorStateImpl value,
$Res Function(_$ColumnsSelectorStateImpl) then) =
__$$ColumnsSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int columns, ViewMode viewMode});
}
/// @nodoc
class __$$ColumnsSelectorStateImplCopyWithImpl<$Res>
extends _$ColumnsSelectorStateCopyWithImpl<$Res, _$ColumnsSelectorStateImpl>
implements _$$ColumnsSelectorStateImplCopyWith<$Res> {
__$$ColumnsSelectorStateImplCopyWithImpl(_$ColumnsSelectorStateImpl _value,
$Res Function(_$ColumnsSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? columns = null,
Object? viewMode = null,
}) {
return _then(_$ColumnsSelectorStateImpl(
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
viewMode: null == viewMode
? _value.viewMode
: viewMode // ignore: cast_nullable_to_non_nullable
as ViewMode,
));
}
}
/// @nodoc
class _$ColumnsSelectorStateImpl implements _ColumnsSelectorState {
const _$ColumnsSelectorStateImpl(
{required this.columns, required this.viewMode});
@override
final int columns;
@override
final ViewMode viewMode;
@override
String toString() {
return 'ColumnsSelectorState(columns: $columns, viewMode: $viewMode)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ColumnsSelectorStateImpl &&
(identical(other.columns, columns) || other.columns == columns) &&
(identical(other.viewMode, viewMode) ||
other.viewMode == viewMode));
}
@override
int get hashCode => Object.hash(runtimeType, columns, viewMode);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl>
get copyWith =>
__$$ColumnsSelectorStateImplCopyWithImpl<_$ColumnsSelectorStateImpl>(
this, _$identity);
}
abstract class _ColumnsSelectorState implements ColumnsSelectorState {
const factory _ColumnsSelectorState(
{required final int columns,
required final ViewMode viewMode}) = _$ColumnsSelectorStateImpl;
@override
int get columns;
@override
ViewMode get viewMode;
@override
@JsonKey(ignore: true)
_$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -124,3 +124,12 @@ class PackageListSelectorState with _$PackageListSelectorState {
required bool isAccessControl,
}) = _PackageListSelectorState;
}
@freezed
class ColumnsSelectorState with _$ColumnsSelectorState {
const factory ColumnsSelectorState({
required int columns,
required ViewMode viewMode,
}) = _ColumnsSelectorState;
}

View File

@@ -14,6 +14,7 @@ class HomePage extends StatelessWidget {
const HomePage({super.key});
_getNavigationBar({
required BuildContext context,
required ViewMode viewMode,
required List<NavigationItem> navigationItems,
required int currentIndex,
@@ -33,90 +34,110 @@ class HomePage extends StatelessWidget {
);
}
final extended = viewMode == ViewMode.desktop;
return NavigationRail(
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: extended,
minExtendedWidth: 172,
selectedIndex: currentIndex,
labelType: extended
? NavigationRailLabelType.none
: NavigationRailLabelType.selected,
// return NavigationRail(
// backgroundColor: context.colorScheme.surfaceContainer,
// groupAlignment: -0.8,
// destinations: navigationItems
// .map(
// (e) => NavigationRailDestination(
// icon: e.icon,
// label: Text(
// Intl.message(e.label),
// ),
// ),
// )
// .toList(),
// onDestinationSelected: globalState.appController.toPage,
// extended: extended,
// minExtendedWidth: 172,
// selectedIndex: currentIndex,
// labelType: extended
// ? NavigationRailLabelType.none
// : NavigationRailLabelType.selected,
// );
return Container(
padding: const EdgeInsets.all(16).copyWith(
right: 0,
),
color: context.colorScheme.surface,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: NavigationRail(
backgroundColor: context.colorScheme.surfaceContainer,
groupAlignment: -0.8,
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: extended,
minExtendedWidth: 172,
selectedIndex: currentIndex,
labelType: extended
? NavigationRailLabelType.none
: NavigationRailLabelType.selected,
),
),
);
}
@override
Widget build(BuildContext context) {
return PopContainer(
child: LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
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"),
),
);
},
),
return LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
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(
context: context,
viewMode: viewMode,
navigationItems: navigationItems,
currentIndex: currentIndex,
);
final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null;
final sideNavigationBar =
viewMode != ViewMode.mobile ? navigationBar : null;
return CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
),
sideNavigationBar: sideNavigationBar,
body: child!,
bottomNavigationBar: bottomNavigationBar,
);
},
child: const HomeBody(
key: Key("home_boy"),
),
);
},
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:proxy/proxy_platform_interface.dart';
@@ -36,7 +37,7 @@ class Proxy extends ProxyPlatform {
return _instance!;
}
Future<bool?> _initService() async {
Future<bool?> initService() async {
return await methodChannel.invokeMethod<bool>("initService");
}
@@ -45,12 +46,11 @@ class Proxy extends ProxyPlatform {
}
@override
Future<bool?> startProxy(port, args) async {
if (!globalState.isVpnService) {
return await _initService();
}
return await methodChannel
.invokeMethod<bool>("startProxy", {'port': port, 'args': args});
Future<bool?> startProxy(port) async {
return await methodChannel.invokeMethod<bool>("startProxy", {
'port': port,
'args': json.encode(clashCore.getProps()),
});
}
@override
@@ -80,6 +80,7 @@ class Proxy extends ProxyPlatform {
bool get isStart => startTime != null && startTime!.isBeforeNow;
onStarted(int? fd) {
debugPrint("onStarted ==> $fd");
if (fd == null) return;
if (receiver != null) {
receiver!.close();

View File

@@ -1,10 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:animations/animations.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
@@ -72,18 +70,20 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) async {
final args = config.isAccessControl
? json.encode(
Props(
accessControl: config.accessControl,
allowBypass: config.allowBypass,
),
)
: null;
await proxyManager.startProxy(
port: clashConfig.mixedPort,
args: args,
);
if (!globalState.isVpnService && Platform.isAndroid) {
clashCore.setProps(
Props(
accessControl: config.isAccessControl ? config.accessControl : null,
allowBypass: config.allowBypass,
systemProxy: config.systemProxy,
),
);
await proxy?.initService();
} else {
await proxyManager.startProxy(
port: clashConfig.mixedPort,
);
}
startListenUpdate();
if (Platform.isAndroid) {
return;
@@ -92,7 +92,7 @@ class GlobalState {
appState: appState,
config: config,
clashConfig: clashConfig,
).then((_){
).then((_) {
globalState.appController.addCheckIpNumDebounce();
});
}
@@ -123,6 +123,15 @@ class GlobalState {
}) async {
appState.isInit = clashCore.isInit;
if (!appState.isInit) {
if(Platform.isAndroid){
clashCore.setProps(
Props(
accessControl: config.isAccessControl ? config.accessControl : null,
allowBypass: config.allowBypass,
systemProxy: config.systemProxy,
),
);
}
appState.isInit = await clashService.init(
config: config,
clashConfig: clashConfig,
@@ -209,30 +218,30 @@ class GlobalState {
required String message,
SnackBarAction? action,
}) {
// final width = context.width;
// EdgeInsets margin;
// if (width < 600) {
// margin = const EdgeInsets.only(
// bottom: 96,
// right: 16,
// left: 16,
// );
// } else {
// margin = EdgeInsets.only(
// bottom: 16,
// left: 16,
// right: width - 316,
// );
// }
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// action: action,
// content: Text(message),
// behavior: SnackBarBehavior.floating,
// duration: const Duration(milliseconds: 1500),
// margin: margin,
// ),
// );
final width = context.width;
EdgeInsets margin;
if (width < 600) {
margin = const EdgeInsets.only(
bottom: 16,
right: 16,
left: 16,
);
} else {
margin = EdgeInsets.only(
bottom: 16,
left: 16,
right: width - 316,
);
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: action,
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(milliseconds: 1500),
margin: margin,
),
);
}
Future<T?> safeRun<T>(
@@ -252,18 +261,6 @@ class GlobalState {
return null;
}
}
int getColumns(ViewMode viewMode, int currentColumns) {
final targetColumnsArray = switch (viewMode) {
ViewMode.mobile => [2, 1],
ViewMode.laptop => [3, 2],
ViewMode.desktop => [4, 3],
};
if (targetColumnsArray.contains(currentColumns)) {
return currentColumns;
}
return targetColumnsArray.first;
}
}
final globalState = GlobalState();

View File

@@ -19,8 +19,7 @@ class AndroidContainer extends StatefulWidget {
class _AndroidContainerState extends State<AndroidContainer>
with WidgetsBindingObserver {
_excludeContainer(Widget child) {
Widget _excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.isExclude,
builder: (_, isExclude, child) {
@@ -31,6 +30,20 @@ class _AndroidContainerState extends State<AndroidContainer>
);
}
Widget _systemUiOverlayContainer(Widget child) {
return AnnotatedRegion(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
child: child,
);
}
@override
void initState() {
super.initState();
@@ -48,7 +61,9 @@ class _AndroidContainerState extends State<AndroidContainer>
@override
Widget build(BuildContext context) {
return _excludeContainer(widget.child);
return _systemUiOverlayContainer(
_excludeContainer(widget.child),
);
}
@override

View File

@@ -3,7 +3,6 @@ import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/plugins/app.dart';
class ClashMessageContainer extends StatefulWidget {
final Widget child;

View File

@@ -0,0 +1,167 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'chip.dart';
import 'list.dart';
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Widget? trailing;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.trailing,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getRequestText(Metadata metadata) {
var text = "${metadata.network}://";
final ips = [
metadata.host,
metadata.destinationIP,
].where((ip) => ip.isNotEmpty);
text += ips.join("/");
text += ":${metadata.destinationPort}";
return text;
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: trailing,
);
}
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, value, child) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if(process.isEmpty) return;
onClick!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null,
title: Text(
_getRequestText(connection.metadata),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
),
trailing: trailing,
);
},
);
}
}

View File

@@ -1,56 +0,0 @@
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/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'side_sheet.dart';
showExtendPage(
BuildContext context, {
required Widget body,
required String title,
double? extendPageWidth,
Widget? action,
}) {
final NavigatorState navigator = Navigator.of(context);
final globalKey = GlobalKey();
final uniqueBody = Container(
key: globalKey,
child: body,
);
navigator.push(
ModalSideSheetRoute(
modalBarrierColor: Colors.black38,
builder: (context) => Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, __) {
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
final commonScaffold = CommonScaffold(
automaticallyImplyLeading: isMobile ? true : false,
actions: isMobile
? null
: [
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
title: title,
body: uniqueBody,
);
return AnimatedContainer(
duration: kThemeAnimationDuration,
width: isMobile ? viewWidth : extendPageWidth ?? 300,
child: commonScaffold,
);
},
),
constraints: const BoxConstraints(),
filter: filter,
),
);
}

View File

@@ -5,7 +5,7 @@ import 'package:fl_clash/widgets/open_container.dart';
import 'package:flutter/material.dart';
import 'card.dart';
import 'extend_page.dart';
import 'sheet.dart';
import 'scaffold.dart';
class Delegate {

View File

@@ -1,5 +1,4 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/system.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -7,7 +6,7 @@ import 'package:flutter/services.dart';
class CommonScaffold extends StatefulWidget {
final Widget body;
final Widget? bottomNavigationBar;
final Widget? floatingActionButton;
final Widget? sideNavigationBar;
final String title;
final Widget? leading;
final List<Widget>? actions;
@@ -16,11 +15,11 @@ class CommonScaffold extends StatefulWidget {
const CommonScaffold({
super.key,
required this.body,
this.sideNavigationBar,
this.bottomNavigationBar,
this.leading,
required this.title,
this.actions,
this.floatingActionButton,
this.automaticallyImplyLeading = true,
});
@@ -51,8 +50,6 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
set actions(List<Widget> actions) {
@@ -61,12 +58,6 @@ 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,
@@ -91,7 +82,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override
void dispose() {
_actions.dispose();
_floatingActionButton.dispose();
super.dispose();
}
@@ -100,89 +90,69 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
_platformContainer({required Widget child}) {
if (system.isDesktop) {
return child;
}
return AnnotatedRegion(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
child: child,
);
}
Widget? get _sideNavigationBar => widget.sideNavigationBar;
@override
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
floatingActionButton: widget.floatingActionButton ??
ValueListenableBuilder(
valueListenable: _floatingActionButton,
builder: (_, floatingActionButton, __) {
return floatingActionButton ?? Container();
final scaffold = Scaffold(
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
ValueListenableBuilder<List<Widget>>(
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
return AppBar(
centerTitle: false,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
actions: [
...?realActions,
const SizedBox(
width: 8,
)
],
);
},
),
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
ValueListenableBuilder(
valueListenable: _loading,
builder: (_, value, __) {
return value == true
? const LinearProgressIndicator()
: Container();
},
),
],
),
),
body: widget.body,
bottomNavigationBar: widget.bottomNavigationBar,
);
return _sideNavigationBar != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder(
valueListenable: _actions,
builder: (_, actions, __) {
return AppBar(
automaticallyImplyLeading: widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
actions: actions.isNotEmpty ? actions : widget.actions,
);
},
),
ValueListenableBuilder(
valueListenable: _loading,
builder: (_, value, __) {
return value == true
? const LinearProgressIndicator()
: Container();
},
_sideNavigationBar!,
Expanded(
flex: 1,
child: Material(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: scaffold,
),
),
),
],
),
),
body: widget.body,
bottomNavigationBar: widget.bottomNavigationBar,
),
);
}
}
class AppIcon extends StatelessWidget {
const AppIcon({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
width: 30,
height: 30,
child: const CircleAvatar(
foregroundImage: AssetImage("assets/images/launch_icon.png"),
backgroundColor: Colors.transparent,
),
);
)
: scaffold;
}
}

106
lib/widgets/sheet.dart Normal file
View File

@@ -0,0 +1,106 @@
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/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'side_sheet.dart';
showExtendPage(
BuildContext context, {
required Widget body,
required String title,
double? extendPageWidth,
Widget? action,
}) {
final NavigatorState navigator = Navigator.of(context);
final globalKey = GlobalKey();
final uniqueBody = Container(
key: globalKey,
child: body,
);
navigator.push(
ModalSideSheetRoute(
modalBarrierColor: Colors.black38,
builder: (context) => Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, __) {
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
final commonScaffold = isMobile
? CommonScaffold(
actions: isMobile
? null
: [
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
title: title,
body: uniqueBody,
)
: Column(
children: [
AppBar(
automaticallyImplyLeading: false,
title: Text(title),
actions: const [
SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
)
],
),
Expanded(
flex: 1,
child: uniqueBody,
),
],
);
return AnimatedContainer(
duration: kThemeAnimationDuration,
width: isMobile ? viewWidth : extendPageWidth ?? 300,
child: commonScaffold,
);
},
),
constraints: const BoxConstraints(),
filter: filter,
),
);
}
showSheet({
required BuildContext context,
required WidgetBuilder builder,
required String title,
bool isScrollControlled = true,
double width = 320,
}) {
final viewMode = globalState.appController.appState.viewMode;
final isMobile = viewMode == ViewMode.mobile;
if (isMobile) {
showModalBottomSheet(
context: context,
isScrollControlled: isScrollControlled,
builder: builder,
showDragHandle: true,
useSafeArea: true,
);
} else {
showModalSideSheet(
useSafeArea: true,
isScrollControlled: isScrollControlled,
context: context,
constraints: BoxConstraints(
maxWidth: width,
),
body: builder(context),
title: title,
);
}
}

View File

@@ -84,8 +84,8 @@ class _SideSheetState extends State<SideSheet> {
borderRadius: BorderRadius.circular(0),
);
final BoxConstraints constraints =
widget.constraints ?? const BoxConstraints(maxWidth: 320);
final BoxConstraints constraints = widget.constraints ??
const BoxConstraints(maxWidth: 320, minWidth: 320);
final Clip clipBehavior = widget.clipBehavior ?? Clip.none;
@@ -403,27 +403,26 @@ class _ModalSideSheetState<T> extends State<_ModalSideSheet<T>> {
}
class ModalSideSheetRoute<T> extends PopupRoute<T> {
ModalSideSheetRoute({
required this.builder,
this.capturedThemes,
this.barrierLabel,
this.barrierOnTapHint,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
this.modalBarrierColor,
this.isDismissible = true,
this.isScrollControlled = false,
this.scrollControlDisabledMaxHeightRatio =
_defaultScrollControlDisabledMaxHeightRatio,
super.settings,
this.transitionAnimationController,
this.anchorPoint,
this.useSafeArea = false,
super.filter
});
ModalSideSheetRoute(
{required this.builder,
this.capturedThemes,
this.barrierLabel,
this.barrierOnTapHint,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
this.modalBarrierColor,
this.isDismissible = true,
this.isScrollControlled = false,
this.scrollControlDisabledMaxHeightRatio =
_defaultScrollControlDisabledMaxHeightRatio,
super.settings,
this.transitionAnimationController,
this.anchorPoint,
this.useSafeArea = false,
super.filter});
final WidgetBuilder builder;
@@ -592,31 +591,19 @@ Future<T?> showModalSideSheet<T>({
builder: (context) {
return Column(
children: [
Flexible(
flex: 0,
child: Row(
children: [
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: BackButton(),
),
const SizedBox(width: 8,),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
),
AppBar(
automaticallyImplyLeading: false,
title: Text(title),
actions: const [
SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
)
],
),
Expanded(
flex: 1,
child: body,
),
],

View File

@@ -35,6 +35,9 @@ class _TrayContainerState extends State<TrayContainer> with TrayListener {
await trayManager.setIcon(
other.getTrayIconPath(),
);
await trayManager.setToolTip(
appName,
);
isTrayInit = true;
}
}
@@ -44,6 +47,9 @@ class _TrayContainerState extends State<TrayContainer> with TrayListener {
await trayManager.setIcon(
other.getTrayIconPath(),
);
await trayManager.setToolTip(
appName,
);
}
updateMenu(TrayContainerSelectorState state) async {

View File

@@ -11,7 +11,7 @@ export 'null_status.dart';
export 'pop_container.dart';
export 'disabled_mask.dart';
export 'side_sheet.dart';
export 'extend_page.dart';
export 'sheet.dart';
export 'keep_container.dart';
export 'animate_grid.dart';
export 'tray_container.dart';
@@ -22,4 +22,5 @@ export 'tile_container.dart';
export 'chip.dart';
export 'fade_box.dart';
export 'app_state_container.dart';
export 'text.dart';
export 'text.dart';
export 'connection_item.dart';

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -18,7 +20,6 @@ class WindowContainer extends StatefulWidget {
}
class _WindowContainerState extends State<WindowContainer> with WindowListener {
_autoLaunchContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.autoLaunch,
@@ -32,7 +33,22 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
@override
Widget build(BuildContext context) {
return _autoLaunchContainer(widget.child);
return Stack(
children: [
Column(
children: [
SizedBox(
height: kHeaderHeight,
),
Expanded(
flex: 1,
child: _autoLaunchContainer(widget.child),
),
],
),
const WindowHeader(),
],
);
}
@override
@@ -47,6 +63,28 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
super.onWindowClose();
}
@override
Future<void> onWindowMoved() async {
super.onWindowMoved();
final offset = await windowManager.getPosition();
final config = globalState.appController.config;
config.windowProps = config.windowProps.copyWith(
top: offset.dy,
left: offset.dx,
);
}
@override
Future<void> onWindowResized() async {
super.onWindowResized();
final size = await windowManager.getSize();
final config = globalState.appController.config;
config.windowProps = config.windowProps.copyWith(
width: size.width,
height: size.height,
);
}
@override
void onWindowMinimize() async {
await globalState.appController.savePreferences();
@@ -59,3 +97,171 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
super.dispose();
}
}
class WindowHeader extends StatefulWidget {
const WindowHeader({super.key});
@override
State<WindowHeader> createState() => _WindowHeaderState();
}
class _WindowHeaderState extends State<WindowHeader> {
final isMaximizedNotifier = ValueNotifier<bool>(false);
final isPinNotifier = ValueNotifier<bool>(false);
@override
void initState() {
super.initState();
_initNotifier();
}
_initNotifier() async {
isMaximizedNotifier.value = await windowManager.isMaximized();
isPinNotifier.value = await windowManager.isAlwaysOnTop();
}
@override
void dispose() {
super.dispose();
isMaximizedNotifier.dispose();
isPinNotifier.dispose();
}
_updateMaximized() {
isMaximizedNotifier.value = !isMaximizedNotifier.value;
switch (isMaximizedNotifier.value) {
case true:
windowManager.maximize();
case false:
windowManager.unmaximize();
}
}
_updatePin() {
isPinNotifier.value = !isPinNotifier.value;
windowManager.setAlwaysOnTop(isPinNotifier.value);
}
_buildActions() {
return Row(
children: [
IconButton(
onPressed: () async {
_updatePin();
},
icon: ValueListenableBuilder(
valueListenable: isPinNotifier,
builder: (_, value, ___) {
return value
? const Icon(
Icons.push_pin,
)
: const Icon(
Icons.push_pin_outlined,
);
},
),
),
IconButton(
onPressed: () {
windowManager.minimize();
},
icon: const Icon(Icons.remove),
),
IconButton(
onPressed: () async {
_updateMaximized();
},
icon: ValueListenableBuilder(
valueListenable: isMaximizedNotifier,
builder: (_, value, ___) {
return value
? const Icon(
Icons.filter_none,
size: 20,
)
: const Icon(
Icons.crop_square,
);
},
),
),
IconButton(
onPressed: () {
windowManager.close();
},
icon: const Icon(Icons.close),
),
// const SizedBox(
// width: 8,
// ),
],
);
}
@override
Widget build(BuildContext context) {
return Material(
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onPanStart: (_) {
windowManager.startDragging();
},
onDoubleTap: () {
_updateMaximized();
},
child: Container(
color: context.colorScheme.surfaceContainerHigh,
alignment: Alignment.centerLeft,
height: kHeaderHeight,
),
),
),
if (!Platform.isMacOS) ...[
const Positioned(
left: 0,
child: AppIcon(),
),
Positioned(
right: 0,
child: _buildActions(),
),
]
],
),
);
}
}
class AppIcon extends StatelessWidget {
const AppIcon({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(left: 8),
child: const Row(
children: [
SizedBox(
width: 28,
height: 28,
child: CircleAvatar(
foregroundImage: AssetImage("assets/images/launch_icon.png"),
backgroundColor: Colors.transparent,
),
),
SizedBox(
width: 8,
),
Text(
appName,
),
],
),
);
}
}

View File

@@ -118,7 +118,7 @@ install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# libclash.so
set(CLASH_DIR "../libclash/linux/amd64")
set(CLASH_DIR "../libclash/linux")
install(FILES "${CLASH_DIR}/libclash.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)

View File

@@ -87,7 +87,7 @@
4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
779829C96DE7998FCC810C37 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7AC277A92B90DE1400E026B1 /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../libclash/macos/amd64/libclash.dylib; sourceTree = "<group>"; };
7AC277A92B90DE1400E026B1 /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../libclash/macos/libclash.dylib; sourceTree = "<group>"; };
7AF070893C29500AB9129D89 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
7D929F2AFD80E155D78F3718 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
@@ -582,7 +582,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/amd64/";
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/";
PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@@ -710,7 +710,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/amd64/";
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/";
PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -732,7 +732,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/amd64/";
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/";
PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;

View File

@@ -9,7 +9,7 @@ class Proxy extends ProxyPlatform {
static String url = "127.0.0.1";
@override
Future<bool?> startProxy(int port, String? args) async {
Future<bool?> startProxy(int port) async {
bool? isStart = false;
switch (Platform.operatingSystem) {
case "macos":
@@ -19,7 +19,7 @@ class Proxy extends ProxyPlatform {
isStart = await _startProxyWithLinux(port);
break;
case "windows":
isStart = await ProxyPlatform.instance.startProxy(port, args);
isStart = await ProxyPlatform.instance.startProxy(port);
break;
}
if (isStart == true) {

View File

@@ -12,7 +12,7 @@ class MethodChannelProxy extends ProxyPlatform {
MethodChannelProxy();
@override
Future<bool?> startProxy(int port, String? args) async {
Future<bool?> startProxy(int port) async {
return await methodChannel.invokeMethod<bool>("StartProxy", {'port': port});
}

View File

@@ -22,7 +22,7 @@ abstract class ProxyPlatform extends PlatformInterface {
DateTime? startTime;
Future<bool?> startProxy(int port, String? args) {
Future<bool?> startProxy(int port) {
throw UnimplementedError('startProxy() has not been implemented.');
}

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.36+202407132
version: 0.8.45+202407201
environment:
sdk: '>=3.1.0 <4.0.0'

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