Compare commits

..

5 Commits

Author SHA1 Message Date
chen08209
35f89fea90 Update core
Fix some issues
2025-01-10 19:08:15 +08:00
chen08209
58acd9c1ab Update changelog 2025-01-09 02:24:30 +00:00
chen08209
ef97ef40a1 Remake dashboard
Optimize theme

Optimize more details

Update flutter version
2025-01-09 10:10:06 +08:00
chen08209
9cb75f4814 Update changelog 2024-12-08 16:50:30 +00:00
chen08209
375c4e0884 Support better window position memory
Add windows arm64 and linux arm64 build script

Optimize some details
2024-12-09 00:37:27 +08:00
122 changed files with 5395 additions and 2070 deletions

View File

@@ -31,9 +31,10 @@
</td>
</tr>
<tr>
<td>macOS (v10.15+)</td>
<td>macOS</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Universal-ea005e.svg?logo=apple"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-arm64.dmg"><img src="https://img.shields.io/badge/DMG-Apple%20Silicon-%23000000.svg?logo=apple"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Intel%20X64-%2300A9E0.svg?logo=apple"></a><br>
</td>
</tr>
<tr>

View File

@@ -67,8 +67,8 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
channel: 'stable'
flutter-version: 3.24.5
channel: stable
cache: true
- name: Get Flutter Dependency
@@ -84,6 +84,70 @@ jobs:
path: ./dist
overwrite: true
changelog:
runs-on: ubuntu-latest
needs: [ build ]
steps:
- name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/heads/main
- name: Generate
if: ${{ !contains(github.ref, '+') }}
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag=""
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then
tag=${tags[$i]}
else
tag=""
fi
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
break
fi
fi
if [ -n "$currentTag" ]; then
echo "## $currentTag" >> NEW_CHANGELOG.md
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done
cat CHANGELOG.md >> NEW_CHANGELOG.md
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
if: ${{ !contains(github.ref, '+') }}
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
echo "Commit pushing"
git config --local user.email "chen08209@gmail.com"
git config --local user.name "chen08209"
git commit -m "Update changelog"
git push
if [ $? -eq 0 ]; then
echo "Push succeeded"
else
echo "Push failed"
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
upload:
permissions: write-all
needs: [ build ]
@@ -177,4 +241,5 @@ jobs:
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/
target-directory: /tmp/

View File

@@ -1,66 +0,0 @@
name: changelog
on:
push:
tags:
- 'v*'
jobs:
changelog:
if: ${{ !contains(github.ref, '+') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag=""
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then
tag=${tags[$i]}
else
tag=""
fi
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
break
fi
fi
if [ -n "$currentTag" ]; then
echo "## $currentTag" >> NEW_CHANGELOG.md
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done
cat CHANGELOG.md >> NEW_CHANGELOG.md
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
echo "Commit pushing"
git config --local user.email "chen08209@gmail.com"
git config --local user.name "chen08209"
git commit -m "Update changelog"
git push
if [ $? -eq 0 ]; then
echo "Push succeeded"
else
echo "Push failed"
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related

View File

@@ -1,3 +1,57 @@
## v0.8.71
- Remake dashboard
- Optimize theme
- Optimize more details
- Update flutter version
- Update changelog
## v0.8.70
- Support better window position memory
- Add windows arm64 and linux arm64 build script
- Optimize some details
## v0.8.69
- Remake desktop
- Optimize change proxy
- Optimize network check
- Fix fallback issues
- Optimize lots of details
- Update change.yaml
- Fix android tile issues
- Fix windows tray issues
- Support setting bypassDomain
- Update flutter version
- Fix android service issues
- Fix macos dock exit button issues
- Add route address setting
- Optimize provider view
- Update changelog
- Update CHANGELOG.md
## v0.8.67
- Add android shortcuts

View File

@@ -34,6 +34,29 @@ on Mobile:
✨ Support subscription link, Dark mode
## Use
### Linux
⚠️ Make sure to install the following dependencies before using them
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android
Support the following actions
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.CHANGE
```
## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
@@ -70,7 +93,7 @@ on Mobile:
3. Run build script
```bash
dart .\setup.dart
dart .\setup.dart windows --arch <arm64 | amd64>
```
- linux
@@ -80,7 +103,7 @@ on Mobile:
2. Run build script
```bash
dart .\setup.dart
dart .\setup.dart linux --arch <arm64 | amd64>
```
- macOS
@@ -90,7 +113,7 @@ on Mobile:
2. Run build script
```bash
dart .\setup.dart
dart .\setup.dart macos --arch <arm64 | amd64>
```
## Star

View File

@@ -10,7 +10,6 @@
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
基于ClashMeta的多平台代理客户端简单易用开源无广告。
on Desktop:
@@ -35,6 +34,29 @@ on Mobile:
✨ 支持一键导入订阅, 深色模式
## Use
### Linux
⚠️ 使用前请确保安装以下依赖
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android
支持下列操作
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.CHANGE
```
## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
@@ -71,7 +93,7 @@ on Mobile:
3. 运行构建脚本
```bash
dart .\setup.dart
dart .\setup.dart windows --arch <arm64 | amd64>
```
- linux
@@ -81,7 +103,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart
dart .\setup.dart linux --arch <arm64 | amd64>
```
- macOS
@@ -91,7 +113,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart
dart .\setup.dart macos --arch <arm64 | amd64>
```
## Star History

View File

@@ -64,14 +64,16 @@
</intent-filter>
</activity>
<!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />-->
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
<activity
android:name=".TempActivity"
android:exported="true"
android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.START" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.STOP" />
@@ -88,7 +90,8 @@
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>

View File

@@ -20,8 +20,6 @@ enum class RunState {
object GlobalState {
private val lock = ReentrantLock()
val runLock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
@@ -47,35 +45,46 @@ object GlobalState {
}
fun handleToggle(context: Context) {
val starting = handleStart(context)
if (!starting) {
handleStop()
}
}
fun handleStart(context: Context): Boolean {
if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING
runLock.lock()
val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
initServiceEngine(context)
}
} else {
handleStop()
return true
}
return false
}
fun handleStop() {
if (runState.value == RunState.START) {
runState.value = RunState.PENDING
runLock.lock()
getCurrentTilePlugin()?.handleStop()
}
}
fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null
runLock.withLock {
serviceEngine?.destroy()
serviceEngine = null
}
}
fun initServiceEngine(context: Context) {
if (serviceEngine != null) return
lock.withLock {
destroyServiceEngine()
destroyServiceEngine()
runLock.withLock {
serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin())

View File

@@ -8,6 +8,10 @@ class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
wrapAction("START") -> {
GlobalState.handleStart(applicationContext)
}
wrapAction("STOP") -> {
GlobalState.handleStop()
}

View File

@@ -23,6 +23,7 @@ import java.io.ByteArrayOutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -179,4 +180,19 @@ suspend fun <T> MethodChannel.awaitResult(
}
})
}
}
fun ReentrantLock.safeLock() {
if (this.isLocked) {
return
}
this.lock()
}
fun ReentrantLock.safeUnlock() {
if (!this.isLocked) {
return
}
this.unlock()
}

View File

@@ -46,8 +46,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var activity: Activity? = null
private var toast: Toast? = null
private lateinit var context: Context
private lateinit var channel: MethodChannel

View File

@@ -16,6 +16,8 @@ import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process
import com.follow.clash.models.VpnOptions
import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
@@ -28,8 +30,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetSocketAddress
import kotlin.concurrent.withLock
import com.follow.clash.models.Process
import com.follow.clash.models.VpnOptions
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
@@ -111,11 +111,9 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"resolverProcess" -> {
val data = call.argument<String>("data")
val process =
if (data != null) Gson().fromJson(
data,
Process::class.java
) else null
val process = if (data != null) Gson().fromJson(
data, Process::class.java
) else null
val metadata = process?.metadata
if (metadata == null) {
result.success(null)
@@ -173,9 +171,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
fun onUpdateNetwork() {
val dns = networks.flatMap { network ->
connectivity?.resolveDns(network) ?: emptyList()
}
.toSet()
.joinToString(",")
}.toSet().joinToString(",")
scope.launch {
withContext(Dispatchers.Main) {
flutterMethodChannel.invokeMethod("dnsChanged", dns)
@@ -239,8 +235,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options)
flutterMethodChannel.invokeMethod(
"started",
fd
"started", fd
)
}
}

View File

@@ -25,7 +25,6 @@ import (
"github.com/samber/lo"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
)
@@ -33,7 +32,7 @@ import (
var (
isRunning = false
runLock sync.Mutex
ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
@@ -352,8 +351,6 @@ func applyConfig(rawConfig *config.RawConfig) error {
if configParams.IsPatch {
patchConfig()
} else {
handleCloseConnectionsUnLock()
runtime.GC()
hub.ApplyConfig(currentConfig)
patchSelectGroup()
}

View File

@@ -65,6 +65,8 @@ const (
closeConnectionMethod Method = "closeConnection"
getExternalProvidersMethod Method = "getExternalProviders"
getExternalProviderMethod Method = "getExternalProvider"
getCountryCodeMethod Method = "getCountryCode"
getMemoryMethod Method = "getMemory"
updateGeoDataMethod Method = "updateGeoData"
updateExternalProviderMethod Method = "updateExternalProvider"
sideLoadExternalProviderMethod Method = "sideLoadExternalProvider"

View File

@@ -1,21 +1,14 @@
module core
go 1.21
go 1.20
replace github.com/metacubex/mihomo => ./Clash.Meta
require github.com/metacubex/mihomo v1.17.1
require (
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.47.0
)
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
require (
github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
@@ -27,13 +20,15 @@ require (
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/enfein/mieru/v3 v3.10.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
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.1.0 // indirect
github.com/go-chi/chi/v5 v5.2.0 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -41,33 +36,35 @@ require (
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/btree v1.1.3 // indirect
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-20240829085014-a3a4c1f04475 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // 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
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // 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.47.1-0.20240909010619-6b38f24bfcc4 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
github.com/metacubex/sing-tun v0.4.5 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
@@ -79,16 +76,15 @@ require (
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // 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.13 // 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/sing v0.5.1 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/samber/lo v1.47.0
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shirou/gopsutil/v4 v4.24.11 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -97,22 +93,24 @@ require (
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.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // 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.28.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

View File

@@ -26,12 +26,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
@@ -40,12 +43,11 @@ 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.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -61,29 +63,27 @@ github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/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.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=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -102,28 +102,26 @@ github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0s
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/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.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y=
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
@@ -137,7 +135,6 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:U
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
@@ -166,20 +163,19 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
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-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=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
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=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -189,17 +185,10 @@ 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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -229,8 +218,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -239,11 +228,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -255,20 +244,18 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=

View File

@@ -8,6 +8,7 @@ import (
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
@@ -17,8 +18,10 @@ import (
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"net"
"runtime"
"sort"
"strconv"
"time"
)
@@ -404,6 +407,25 @@ func handleStopLog() {
}
}
func handleGetCountryCode(ip string, fn func(value string)) {
go func() {
runLock.Lock()
defer runLock.Unlock()
codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip))
if len(codes) == 0 {
fn("")
return
}
fn(codes[0])
}()
}
func handleGetMemory(fn func(value string)) {
go func() {
fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10))
}()
}
func init() {
adapter.UrlTestHook = func(name string, delay uint16) {
delayData := &Delay{

View File

@@ -120,6 +120,14 @@ func getConnections() *C.char {
return C.CString(handleGetConnections())
}
//export getMemory
func getMemory(port C.longlong) {
i := int64(port)
handleGetMemory(func(value string) {
bridge.SendToPort(i, value)
})
}
//export closeConnections
func closeConnections() {
handleCloseConnections()
@@ -161,6 +169,15 @@ func updateExternalProvider(providerNameChar *C.char, port C.longlong) {
})
}
//export getCountryCode
func getCountryCode(ipChar *C.char, port C.longlong) {
ip := C.GoString(ipChar)
i := int64(port)
handleGetCountryCode(ip, func(value string) {
bridge.SendToPort(i, value)
})
}
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) {
i := int64(port)

View File

@@ -157,6 +157,17 @@ func handleAction(action *Action) {
case stopListenerMethod:
action.callback(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
action.callback(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
action.callback(value)
})
return
}
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:animations/animations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
@@ -59,22 +58,15 @@ class Application extends StatefulWidget {
class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes;
Timer? timer;
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.windows: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.linux: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.macOS: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.android: CommonPageTransitionsBuilder(),
TargetPlatform.windows: CommonPageTransitionsBuilder(),
TargetPlatform.linux: CommonPageTransitionsBuilder(),
TargetPlatform.macOS: CommonPageTransitionsBuilder(),
},
);
@@ -96,7 +88,8 @@ class ApplicationState extends State<Application> {
@override
void initState() {
super.initState();
_initTimer();
_autoUpdateGroupTask();
_autoUpdateProfilesTask();
globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
@@ -110,29 +103,29 @@ class ApplicationState extends State<Application> {
});
}
_initTimer() {
_cancelTimer();
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
_autoUpdateGroupTask() {
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateGroupDebounce();
globalState.appController.updateGroupsDebounce();
_autoUpdateGroupTask();
});
});
}
_cancelTimer() {
if (timer != null) {
timer?.cancel();
timer = null;
}
_autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles();
_autoUpdateProfilesTask();
});
}
_buildApp(Widget app) {
_buildPlatformWrap(Widget child) {
if (system.isDesktop) {
return WindowManager(
child: TrayManager(
child: HotKeyManager(
child: ProxyManager(
child: app,
child: child,
),
),
),
@@ -140,7 +133,7 @@ class ApplicationState extends State<Application> {
}
return AndroidManager(
child: TileManager(
child: app,
child: child,
),
);
}
@@ -156,6 +149,17 @@ class ApplicationState extends State<Application> {
);
}
_buildWrap(Widget child) {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
onConnectivityChanged: globalState.appController.updateLocalIp,
child: child,
),
),
);
}
_updateSystemColorSchemes(
ColorScheme? lightDynamic,
ColorScheme? darkDynamic,
@@ -171,31 +175,31 @@ class ApplicationState extends State<Application> {
@override
Widget build(context) {
return _buildApp(
AppStateManager(
child: ClashManager(
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale,
themeMode: config.themeProps.themeMode,
primaryColor: config.themeProps.primaryColor,
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
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
],
builder: (_, child) {
return LayoutBuilder(
return _buildWrap(
_buildPlatformWrap(
Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale,
themeMode: config.themeProps.themeMode,
primaryColor: config.themeProps.primaryColor,
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
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
],
builder: (_, child) {
return MessageManager(
child: LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
@@ -204,41 +208,40 @@ class ApplicationState extends State<Application> {
}
return _buildPage(child!);
},
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales:
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
),
home: child,
);
},
);
},
child: const HomePage(),
),
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
),
home: child,
);
},
);
},
child: const HomePage(),
),
),
);
@@ -247,7 +250,8 @@ class ApplicationState extends State<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
_cancelTimer();
_autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel();
await clashService?.destroy();
await globalState.appController.savePreferences();
await globalState.appController.handleExit();

View File

@@ -200,11 +200,27 @@ class ClashCore {
return Traffic.fromMap(json.decode(trafficString));
}
Future<IpInfo?> getCountryCode(String ip) async {
final countryCode = await clashInterface.getCountryCode(ip);
if (countryCode.isEmpty) {
return null;
}
return IpInfo(
ip: ip,
countryCode: countryCode,
);
}
Future<Traffic> getTotalTraffic(bool value) async {
final totalTrafficString = await clashInterface.getTotalTraffic(value);
return Traffic.fromMap(json.decode(totalTrafficString));
}
Future<int> getMemory() async {
final value = await clashInterface.getMemory();
return int.parse(value);
}
resetTraffic() {
clashInterface.resetTraffic();
}

View File

@@ -2348,20 +2348,6 @@ class ClashFFI {
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
void updateDns(
ffi.Pointer<ffi.Char> s,
) {
return _updateDns(
s,
);
}
late final _updateDnsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'updateDns');
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
@@ -2581,6 +2567,18 @@ class ClashFFI {
late final _getConnections =
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void getMemory(
int port,
) {
return _getMemory(
port,
);
}
late final _getMemoryPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('getMemory');
late final _getMemory = _getMemoryPtr.asFunction<void Function(int)>();
void closeConnections() {
return _closeConnections();
}
@@ -2665,6 +2663,23 @@ class ClashFFI {
late final _updateExternalProvider = _updateExternalProviderPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void getCountryCode(
ffi.Pointer<ffi.Char> ipChar,
int port,
) {
return _getCountryCode(
ipChar,
port,
);
}
late final _getCountryCodePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('getCountryCode');
late final _getCountryCode = _getCountryCodePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerNameChar,
ffi.Pointer<ffi.Char> dataChar,
@@ -2793,6 +2808,20 @@ class ClashFFI {
'setState');
late final _setState =
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void updateDns(
ffi.Pointer<ffi.Char> s,
) {
return _updateDns(
s,
);
}
late final _updateDnsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'updateDns');
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
}
final class __mbstate_t extends ffi.Union {

View File

@@ -45,6 +45,10 @@ mixin ClashInterface {
FutureOr<String> getTotalTraffic(bool value);
FutureOr<String> getCountryCode(String ip);
FutureOr<String> getMemory();
resetTraffic();
startLog();

View File

@@ -306,6 +306,39 @@ class ClashLib with ClashInterface {
clashFFI.forceGc();
}
@override
FutureOr<String> getCountryCode(String ip) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final ipChar = ip.toNativeUtf8().cast<Char>();
clashFFI.getCountryCode(
ipChar,
receiver.sendPort.nativePort,
);
malloc.free(ipChar);
return completer.future;
}
@override
FutureOr<String> getMemory() {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.getMemory(receiver.sendPort.nativePort);
return completer.future;
}
/// Android
startTun(int fd, int port) {

View File

@@ -138,6 +138,8 @@ class ClashService with ClashInterface {
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
case ActionMethod.getCountryCode:
case ActionMethod.getMemory:
completer?.complete(action.data as String);
return;
case ActionMethod.message:
@@ -146,7 +148,6 @@ class ClashService with ClashInterface {
case ActionMethod.forceGc:
case ActionMethod.startLog:
case ActionMethod.stopLog:
default:
return;
}
}
@@ -174,7 +175,16 @@ class ClashService with ClashInterface {
onLast: () {
callbackCompleterMap.remove(id);
},
onTimeout: onTimeout,
onTimeout: onTimeout ??
() {
if (T is String) {
return "" as T;
}
if (T is bool) {
return false as T;
}
return null as T;
},
functionName: id,
);
}
@@ -409,6 +419,21 @@ class ClashService with ClashInterface {
await server.close();
await _deleteSocketFile();
}
@override
FutureOr<String> getCountryCode(String ip) {
return _invoke<String>(
method: ActionMethod.getCountryCode,
data: ip,
);
}
@override
FutureOr<String> getMemory() {
return _invoke<String>(
method: ActionMethod.getMemory,
);
}
}
final clashService = system.isDesktop ? ClashService() : null;

View File

@@ -1,19 +1,20 @@
import 'package:flutter/material.dart';
extension ColorExtension on Color {
toLight() {
Color get toLight {
return withOpacity(0.8);
}
Color get toLighter {
return withOpacity(0.6);
}
toLighter() {
return withOpacity(0.4);
}
toSoft() {
Color get toSoft {
return withOpacity(0.12);
}
toLittle() {
Color get toLittle {
return withOpacity(0.03);
}
@@ -23,13 +24,39 @@ extension ColorExtension on Color {
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
Color blendDarken(
BuildContext context, {
double factor = 0.1,
}) {
final brightness = Theme.of(context).brightness;
return Color.lerp(
this,
brightness == Brightness.dark ? Colors.white : Colors.black,
factor,
)!;
}
Color blendLighten(
BuildContext context, {
double factor = 0.1,
}) {
final brightness = Theme.of(context).brightness;
return Color.lerp(
this,
brightness == Brightness.dark ? Colors.black : Colors.white,
factor,
)!;
}
}
extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
surfaceContainer: surfaceContainer.darken(0.05),
surfaceContainer: surfaceContainer.darken(
0.05,
),
)
: this;
}

View File

@@ -15,9 +15,14 @@ const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const helperTag = "2024125";
const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
);
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
const commonDuration = Duration(milliseconds: 300);
const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
@@ -79,3 +84,7 @@ const viewModeColumnsMap = {
};
const defaultPrimaryColor = Colors.brown;
double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0);
}

View File

@@ -1,13 +1,17 @@
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
extension BuildContextExtension on BuildContext {
CommonScaffoldState? get commonScaffoldState {
return findAncestorStateOfType<CommonScaffoldState>();
}
Size get appSize{
showNotifier(String text) {
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
Size get appSize {
return MediaQuery.of(this).size;
}

View File

@@ -1,26 +1,33 @@
import 'dart:async';
class Debouncer {
final Duration delay;
Timer? _timer;
Map<dynamic, Timer> operators = {};
Debouncer({required this.delay});
call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = operators[tag];
if (timer != null) {
timer.cancel();
}
operators[tag] = Timer(
duration,
() {
operators.remove(tag);
Function.apply(
func,
args,
);
},
);
}
void call(Function action, List<dynamic> positionalArguments, [Map<Symbol, dynamic>? namedArguments]) {
_timer?.cancel();
_timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments));
cancel(dynamic tag) {
operators[tag]?.cancel();
}
}
Function debounce<F extends Function>(F func,{int milliseconds = 600}) {
Timer? timer;
return ([List<dynamic>? args, Map<Symbol, dynamic>? namedArgs]) {
if (timer != null) {
timer!.cancel();
}
timer = Timer(Duration(milliseconds: milliseconds), () async {
await Function.apply(func, args ?? [], namedArgs);
});
};
}
final debouncer = Debouncer();

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
extension CompleterExt<T> on Completer<T> {
safeFuture({
Duration? timeout,
@@ -8,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
FutureOr<T> Function()? onTimeout,
required String functionName,
}) {
final realTimeout = timeout ?? const Duration(seconds: 6);
Timer(realTimeout + Duration(milliseconds: 1000), () {
final realTimeout = timeout ?? const Duration(minutes: 1);
Timer(realTimeout + moreDuration, () {
if (onLast != null) {
onLast();
}

View File

@@ -14,10 +14,10 @@ class FlClashHttpOverrides extends HttpOverrides {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
debugPrint("find $url");
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/models/models.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart';
@@ -34,8 +33,7 @@ class AutoLaunch {
return await launchAtStartup.disable();
}
updateStatus(AutoLaunchState state) async {
final isAutoLaunch = state.isAutoLaunch;
updateStatus(bool isAutoLaunch) async {
if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) {
enable();

View File

@@ -1,11 +1,251 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
return await Navigator.of(context).push<T>(
MaterialPageRoute(
CommonRoute(
builder: (context) => child,
),
);
}
}
class CommonRoute<T> extends MaterialPageRoute<T> {
CommonRoute({
required super.builder,
});
@override
Duration get transitionDuration => const Duration(milliseconds: 500);
@override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
}
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
);
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0 / 3.0, 0.0),
);
class CommonPageTransitionsBuilder extends PageTransitionsBuilder {
const CommonPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return CommonPageTransition(
context: context,
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: false,
child: child,
);
}
}
class CommonPageTransition extends StatefulWidget {
const CommonPageTransition({
super.key,
required this.context,
required this.primaryRouteAnimation,
required this.secondaryRouteAnimation,
required this.child,
required this.linearTransition,
});
final Widget child;
final Animation<double> primaryRouteAnimation;
final Animation<double> secondaryRouteAnimation;
final BuildContext context;
final bool linearTransition;
static Widget? delegatedTransition(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
bool allowSnapshotting,
Widget? child) {
final Animation<Offset> delegatedPositionAnimation = CurvedAnimation(
parent: secondaryAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(_kMiddleLeftTween);
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: delegatedPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: child,
);
}
@override
State<CommonPageTransition> createState() => _CommonPageTransitionState();
}
class _CommonPageTransitionState extends State<CommonPageTransition> {
late Animation<Offset> _primaryPositionAnimation;
late Animation<Offset> _secondaryPositionAnimation;
late Animation<Decoration> _primaryShadowAnimation;
CurvedAnimation? _primaryPositionCurve;
CurvedAnimation? _secondaryPositionCurve;
CurvedAnimation? _primaryShadowCurve;
@override
void initState() {
super.initState();
_setupAnimation();
}
@override
void didUpdateWidget(covariant CommonPageTransition oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
oldWidget.linearTransition != widget.linearTransition) {
_disposeCurve();
_setupAnimation();
}
}
@override
void dispose() {
_disposeCurve();
super.dispose();
}
void _disposeCurve() {
_primaryPositionCurve?.dispose();
_secondaryPositionCurve?.dispose();
_primaryShadowCurve?.dispose();
_primaryPositionCurve = null;
_secondaryPositionCurve = null;
_primaryShadowCurve = null;
}
void _setupAnimation() {
if (!widget.linearTransition) {
_primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.fastEaseInToSlowEaseOut,
reverseCurve: Curves.easeInOut,
);
_secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
);
_primaryShadowCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.linearToEaseOut,
);
}
_primaryPositionAnimation =
(_primaryPositionCurve ?? widget.primaryRouteAnimation)
.drive(_kRightMiddleTween);
_secondaryPositionAnimation =
(_secondaryPositionCurve ?? widget.secondaryRouteAnimation)
.drive(_kMiddleLeftTween);
_primaryShadowAnimation =
(_primaryShadowCurve ?? widget.primaryRouteAnimation).drive(
DecorationTween(
begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration(
<Color>[
widget.context.colorScheme.inverseSurface.withOpacity(
0.06,
),
Colors.transparent,
],
),
),
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: _secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: _primaryPositionAnimation,
textDirection: textDirection,
child: DecoratedBoxTransition(
decoration: _primaryShadowAnimation,
child: widget.child,
),
),
);
}
}
class _CommonEdgeShadowDecoration extends Decoration {
final List<Color>? _colors;
const _CommonEdgeShadowDecoration([this._colors]);
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _CommonEdgeShadowPainter(this, onChanged);
}
}
class _CommonEdgeShadowPainter extends BoxPainter {
_CommonEdgeShadowPainter(
this._decoration,
super.onChanged,
) : assert(_decoration._colors == null || _decoration._colors!.length > 1);
final _CommonEdgeShadowDecoration _decoration;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final List<Color>? colors = _decoration._colors;
if (colors == null) {
return;
}
final double shadowWidth = 0.05 * configuration.size!.width;
final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1);
final TextDirection? textDirection = configuration.textDirection;
assert(textDirection != null);
final (double shadowDirection, double start) = switch (textDirection!) {
TextDirection.rtl => (1, offset.dx + configuration.size!.width),
TextDirection.ltr => (-1, offset.dx),
};
int bandColorIndex = 0;
for (int dx = 0; dx < shadowWidth; dx += 1) {
if (dx ~/ bandWidth != bandColorIndex) {
bandColorIndex += 1;
}
final Paint paint = Paint()
..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1],
(dx % bandWidth) / bandWidth)!;
final double x = start + shadowDirection * dx;
canvas.drawRect(
Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
}
}
}

View File

@@ -1,5 +1,43 @@
extension NumExtension on num {
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
extension NumExt on num {
String fixed({digit = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
}
}
extension DoubleExt on double {
moreOrEqual(double value) {
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
}
}
extension OffsetExt on Offset {
double getCrossAxisOffset(Axis direction) {
return direction == Axis.vertical ? dx : dy;
}
double getMainAxisOffset(Axis direction) {
return direction == Axis.vertical ? dy : dx;
}
bool less(Offset offset) {
if (dy < offset.dy) {
return true;
}
if (dy == offset.dy && dx < offset.dx) {
return true;
}
return false;
}
}
extension RectExt on Rect {
doRectIntersect(Rect rect) {
return left < rect.right &&
right > rect.left &&
top < rect.bottom &&
bottom > rect.top;
}
}

View File

@@ -34,6 +34,19 @@ class Other {
);
}
String get uuidV4 {
final Random random = Random();
final bytes = List.generate(16, (_) => random.nextInt(256));
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
final hex =
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}';
}
String getTimeDifference(DateTime dateTime) {
var currentDateTime = DateTime.now();
var difference = currentDateTime.difference(dateTime);
@@ -116,13 +129,11 @@ class Other {
return "assets/images/icon_white.png";
}
final suffix = Platform.isWindows ? "ico" : "png";
if (Platform.isWindows) {
return "assets/images/icon.$suffix";
}
return switch (brightness) {
Brightness.dark => "assets/images/icon_white.$suffix",
Brightness.light => "assets/images/icon_black.$suffix",
};
return "assets/images/icon.$suffix";
// return switch (brightness) {
// Brightness.dark => "assets/images/icon_white.$suffix",
// Brightness.light => "assets/images/icon_black.$suffix",
// };
}
int compareVersions(String version1, String version2) {
@@ -227,7 +238,7 @@ class Other {
}
int getProfilesColumns(double viewWidth) {
return max((viewWidth / 400).floor(), 1);
return max((viewWidth / 350).floor(), 1);
}
String getBackupFileName() {
@@ -242,6 +253,32 @@ class Other {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,
)
..sort((a, b) {
if (a.isWifi && !b.isWifi) return -1;
if (!a.isWifi && b.isWifi) return 1;
if (a.includesIPv4 && !b.includesIPv4) return -1;
if (!a.includesIPv4 && b.includesIPv4) return 1;
return 0;
});
for (final interface in interfaces) {
final addresses = interface.addresses;
if (addresses.isEmpty) {
continue;
}
addresses.sort((a, b) {
if (a.isIPv4 && !b.isIPv4) return -1;
if (!a.isIPv4 && b.isIPv4) return 1;
return 0;
});
return addresses.first.address;
}
return "";
}
}
final other = Other();

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -70,28 +71,34 @@ class Request {
return data;
}
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
final List<String> _ipInfoSources = [
"https://ipwho.is/?fields=ip&output=csv",
"https://ipinfo.io/ip",
"https://ifconfig.me/ip/",
];
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) {
for (final source in _ipInfoSources) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.get<String>(
source,
cancelToken: cancelToken,
)
.timeout(httpTimeoutDuration);
if (response.statusCode != 200 || response.data == null) {
continue;
}
return source.value(response.data!);
final ipInfo = await clashCore.getCountryCode(response.data!);
if (ipInfo == null && source != _ipInfoSources.last) {
continue;
}
return ipInfo;
} catch (e) {
debugPrint("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled";
}
debugPrint("checkIp error ===> $e");
}
}
return null;

View File

@@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
import 'color.dart';
extension TextStyleExtension on TextStyle {
TextStyle get toLight => copyWith(color: color?.toLight());
TextStyle get toLight => copyWith(color: color?.toLight);
TextStyle get toLighter => copyWith(color: color?.toLighter());
TextStyle get toLighter => copyWith(color: color?.toLighter);
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
TextStyle get toMinus => copyWith(fontSize: fontSize! - 2);
TextStyle adjustSize(int size) => copyWith(
fontSize: fontSize! + size,
);
}

View File

@@ -78,7 +78,7 @@ class Tray {
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
globalState.appController.changeMode(mode);
},
checked: mode == clashConfig.mode,
),

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart';
class Window {
@@ -23,6 +24,35 @@ class Window {
);
if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
final left = props.left ?? 0;
final top = props.top ?? 0;
final right = left + props.width;
final bottom = top + props.height;
if (left == 0 && top == 0) {
await windowManager.setAlignment(Alignment.center);
} else {
final displays = await screenRetriever.getAllDisplays();
final isPositionValid = displays.any(
(display) {
final displayBounds = Rect.fromLTWH(
display.visiblePosition!.dx,
display.visiblePosition!.dy,
display.size.width,
display.size.height,
);
return displayBounds.contains(Offset(left, top)) ||
displayBounds.contains(Offset(right, bottom));
},
);
if (isPositionValid) {
await windowManager.setPosition(
Offset(
left,
top,
),
);
}
}
}
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true);

View File

@@ -23,40 +23,47 @@ class AppController {
late AppFlowingState appFlowingState;
late Config config;
late ClashConfig clashConfig;
late Function updateClashConfigDebounce;
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
late Function savePreferencesDebounce;
late Function changeProxyDebounce;
AppController(this.context) {
appState = context.read<AppState>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
appFlowingState = context.read<AppFlowingState>();
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
}
updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
}
updateGroupsDebounce() {
debouncer.call(DebounceTag.updateGroups, updateGroups);
}
addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
appState.checkIpNum++;
});
savePreferencesDebounce = debounce<Function()>(() async {
await savePreferences();
}
applyProfileDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
applyProfile(isPrue: true);
});
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
changeProxyDebounce = debounce((String groupName, String proxyName) async {
}
savePreferencesDebounce() {
debouncer.call(DebounceTag.savePreferences, savePreferences);
}
changeProxyDebounce(String groupName, String proxyName) {
debouncer.call(DebounceTag.changeProxy,
(String groupName, String proxyName) async {
await changeProxy(
groupName: groupName,
proxyName: proxyName,
);
await updateGroups();
});
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
updateGroupDebounce = debounce(() async {
await updateGroups();
});
}, args: [groupName, proxyName]);
}
restartCore() async {
@@ -94,9 +101,6 @@ class AppController {
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
await Future.delayed(
Duration(milliseconds: 300),
);
addCheckIpNumDebounce();
}
}
@@ -139,8 +143,14 @@ class AppController {
}
}
updateProviders() {
globalState.updateProviders(appState);
updateProviders() async {
await globalState.updateProviders(appState);
}
updateLocalIp() async {
appFlowingState.localIp = null;
await Future.delayed(commonDuration);
appFlowingState.localIp = await other.getLocalIpAddress();
}
Future<void> updateProfile(Profile profile) async {
@@ -148,6 +158,9 @@ class AppController {
config.setProfile(
newProfile.copyWith(isUpdating: false),
);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
}
}
Future<void> updateClashConfig({bool isPatch = true}) async {
@@ -333,6 +346,9 @@ class AppController {
config: config,
);
await _initStatus();
autoLaunch?.updateStatus(
config.appSetting.autoLaunch,
);
autoUpdateProfiles();
autoCheckUpdate();
}
@@ -341,10 +357,12 @@ class AppController {
if (Platform.isAndroid) {
globalState.updateStartTime();
}
if (globalState.isStart) {
await updateStatus(true);
} else {
await updateStatus(config.appSetting.autoRun);
final status =
globalState.isStart == true ? true : config.appSetting.autoRun;
await updateStatus(status);
if (!status) {
addCheckIpNumDebounce();
}
}
@@ -406,10 +424,6 @@ class AppController {
);
}
showSnackBar(String message) {
globalState.showSnackBar(context, message: message);
}
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
dismissible: false,
@@ -582,6 +596,14 @@ class AppController {
updateStatus(!appFlowingState.isStart);
}
changeMode(Mode mode) {
clashConfig.mode = mode;
if (mode == Mode.global) {
config.updateCurrentGroupName(GroupName.GLOBAL.name);
}
addCheckIpNumDebounce();
}
updateAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
autoLaunch: !config.appSetting.autoLaunch,

View File

@@ -1,9 +1,39 @@
// ignore_for_file: constant_identifier_names
import 'dart:io';
import 'package:fl_clash/fragments/dashboard/widgets/widgets.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
enum SupportPlatform {
Windows,
MacOS,
Linux,
Android;
static SupportPlatform get currentPlatform {
if (Platform.isWindows) {
return SupportPlatform.Windows;
} else if (Platform.isMacOS) {
return SupportPlatform.MacOS;
} else if (Platform.isLinux) {
return SupportPlatform.Linux;
} else if (Platform.isAndroid) {
return SupportPlatform.Android;
}
throw "invalid platform";
}
}
const desktopPlatforms = [
SupportPlatform.Linux,
SupportPlatform.MacOS,
SupportPlatform.Windows,
];
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -91,6 +121,10 @@ enum RecoveryOption {
enum ChipType { action, delete }
enum CommonCardType { plain, filled }
//
// extension CommonCardTypeExt on CommonCardType {
// CommonCardType get variant => CommonCardType.plain;
// }
enum ProxiesType { tab, list }
@@ -205,6 +239,8 @@ enum ActionMethod {
stopLog,
startListener,
stopListener,
getCountryCode,
getMemory,
}
enum AuthorizeCode { none, success, error }
@@ -214,3 +250,86 @@ enum WindowsHelperServiceStatus {
presence,
running,
}
enum DebounceTag {
updateClashConfig,
updateGroups,
addCheckIpNum,
applyProfile,
savePreferences,
changeProxy,
checkIp,
handleWill,
updateDelay,
vpnTip,
autoLaunch
}
enum DashboardWidget {
networkSpeed(
GridItem(
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
),
outboundMode(
GridItem(
crossAxisCellCount: 4,
child: OutboundMode(),
),
),
trafficUsage(
GridItem(
crossAxisCellCount: 4,
child: TrafficUsage(),
),
),
networkDetection(
GridItem(
crossAxisCellCount: 4,
child: NetworkDetection(),
),
),
tunButton(
GridItem(
crossAxisCellCount: 4,
child: TUNButton(),
),
platforms: desktopPlatforms,
),
systemProxyButton(
GridItem(
crossAxisCellCount: 4,
child: SystemProxyButton(),
),
platforms: desktopPlatforms,
),
intranetIp(
GridItem(
crossAxisCellCount: 4,
child: IntranetIP(),
),
),
memoryInfo(
GridItem(
crossAxisCellCount: 4,
child: MemoryInfo(),
),
);
final GridItem widget;
final List<SupportPlatform> platforms;
const DashboardWidget(
this.widget, {
this.platforms = SupportPlatform.values,
});
static DashboardWidget getDashboardWidget(GridItem gridItem) {
final dashboardWidgets = DashboardWidget.values;
final index = dashboardWidgets.indexWhere(
(item) => item.widget == gridItem,
);
return dashboardWidgets[index];
}
}

View File

@@ -104,11 +104,9 @@ class _AccessFragmentState extends State<AccessFragment> {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
builder: (_) {
return AccessControlWidget(
context: context,
);
},
body: AccessControlWidget(
context: context,
),
);
},
icon: const Icon(Icons.tune),
@@ -178,8 +176,8 @@ class _AccessFragmentState extends State<AccessFragment> {
status: !isAccessControl,
child: Column(
children: [
AbsorbPointer(
absorbing: !isAccessControl,
ActivateBox(
active: isAccessControl,
child: Padding(
padding: const EdgeInsets.only(
top: 4,
@@ -332,8 +330,8 @@ class PackageListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: !isActive,
return ActivateBox(
active: isActive,
child: ListItem.checkbox(
leading: SizedBox(
width: 48,

View File

@@ -343,8 +343,8 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
@override
void dispose() {
super.dispose();
_obscureController.dispose();
super.dispose();
}
@override

View File

@@ -38,6 +38,9 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) async {
if (!context.mounted) {
return;
}
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
@@ -63,9 +66,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () async {
clashCore.closeConnections();
@@ -109,11 +109,11 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
@override
void dispose() {
super.dispose();
timer?.cancel();
connectionsNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override

View File

@@ -1,18 +1,12 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/fragments/dashboard/status_button.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 'network_detection.dart';
import 'network_speed.dart';
import 'outbound_mode.dart';
import 'start_button.dart';
import 'traffic_usage.dart';
import 'widgets/start_button.dart';
class DashboardFragment extends StatefulWidget {
const DashboardFragment({super.key});
@@ -22,7 +16,9 @@ class DashboardFragment extends StatefulWidget {
}
class _DashboardFragmentState extends State<DashboardFragment> {
_initFab(bool isCurrent) {
final key = GlobalKey<SuperGridState>();
_initScaffold(bool isCurrent) {
if (!isCurrent) {
return;
}
@@ -30,6 +26,47 @@ class _DashboardFragmentState extends State<DashboardFragment> {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = const StartButton();
commonScaffoldState?.actions = [
ValueListenableBuilder(
valueListenable: key.currentState!.addedChildrenNotifier,
builder: (_, addedChildren, child) {
return ValueListenableBuilder(
valueListenable: key.currentState!.isEditNotifier,
builder: (_, isEdit, child) {
if (!isEdit || addedChildren.isEmpty) {
return Container();
}
return child!;
},
child: child,
);
},
child: IconButton(
onPressed: () {
key.currentState!.showAddModal();
},
icon: Icon(
Icons.add_circle,
),
),
),
IconButton(
icon: ValueListenableBuilder(
valueListenable: key.currentState!.isEditNotifier,
builder: (_, isEdit, ___) {
return isEdit
? Icon(Icons.save)
: Icon(
Icons.edit,
);
},
),
onPressed: () {
key.currentState!.isEditNotifier.value =
!key.currentState!.isEditNotifier.value;
},
),
];
});
}
@@ -38,7 +75,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
return ActiveBuilder(
label: "dashboard",
builder: (isCurrent, child) {
_initFab(isCurrent);
_initScaffold(isCurrent);
return child!;
},
child: Align(
@@ -47,52 +84,52 @@ class _DashboardFragmentState extends State<DashboardFragment> {
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, ___) {
final columns = max(4 * ((viewWidth / 350).ceil()), 8);
final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
return Grid(
child: Selector2<AppState, Config, DashboardState>(
selector: (_, appState, config) => DashboardState(
dashboardWidgets: config.appSetting.dashboardWidgets,
viewWidth: appState.viewWidth,
),
builder: (_, state, ___) {
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
return SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
const GridItem(
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
// if (Platform.isAndroid)
// GridItem(
// crossAxisCellCount: switchCount,
// child: const VPNSwitch(),
// ),
if (system.isDesktop) ...[
GridItem(
crossAxisCellCount: switchCount,
child: const TUNButton(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const SystemProxyButton(),
),
],
const GridItem(
crossAxisCellCount: 4,
child: OutboundMode(),
),
const GridItem(
crossAxisCellCount: 4,
child: NetworkDetection(),
),
const GridItem(
crossAxisCellCount: 4,
child: TrafficUsage(),
),
const GridItem(
crossAxisCellCount: 4,
child: IntranetIP(),
),
...state.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
],
onSave: (girdItems) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
dashboardWidgets: dashboardWidgets,
);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
},
);
},
),

View File

@@ -1,140 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class IntranetIP extends StatefulWidget {
const IntranetIP({super.key});
@override
State<IntranetIP> createState() => _IntranetIPState();
}
class _IntranetIPState extends State<IntranetIP> {
final ipNotifier = ValueNotifier<String?>("");
late StreamSubscription subscription;
Future<String> getNetworkType() async {
try {
final interfaces = await NetworkInterface.list(
includeLoopback: false,
type: InternetAddressType.any,
);
for (var interface in interfaces) {
if (interface.name.toLowerCase().contains('wlan') ||
interface.name.toLowerCase().contains('wi-fi')) {
return 'WiFi';
}
if (interface.name.toLowerCase().contains('rmnet') ||
interface.name.toLowerCase().contains('ccmni') ||
interface.name.toLowerCase().contains('cellular')) {
return 'Mobile Data';
}
}
return 'Unknown';
} catch (e) {
return 'Error';
}
}
Future<String?> getLocalIpAddress() async {
await Future.delayed(animateDuration);
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,
)
..sort((a, b) {
if (a.isWifi && !b.isWifi) return -1;
if (!a.isWifi && b.isWifi) return 1;
if (a.includesIPv4 && !b.includesIPv4) return -1;
if (!a.includesIPv4 && b.includesIPv4) return 1;
return 0;
});
for (final interface in interfaces) {
final addresses = interface.addresses;
if (addresses.isEmpty) {
continue;
}
addresses.sort((a, b) {
if (a.isIPv4 && !b.isIPv4) return -1;
if (!a.isIPv4 && b.isIPv4) return 1;
return 0;
});
return addresses.first.address;
}
return null;
}
@override
void initState() {
super.initState();
subscription = Connectivity().onConnectivityChanged.listen((_) async {
ipNotifier.value = null;
debugPrint("[App] Connection change");
ipNotifier.value = await getLocalIpAddress() ?? "";
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
ipNotifier.value = await getLocalIpAddress() ?? "";
});
}
@override
Widget build(BuildContext context) {
return CommonCard(
info: Info(
label: appLocalizations.intranetIP,
iconData: Icons.devices,
),
onPressed: () {},
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.measure.titleMediumHeight + 24 - 2,
child: ValueListenableBuilder(
valueListenable: ipNotifier,
builder: (_, value, __) {
return FadeBox(
child: value != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
value.isNotEmpty
? value
: appLocalizations.noNetwork,
style: context
.textTheme.titleLarge?.toSoftBold.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
)
: const Padding(
padding: EdgeInsets.all(2),
child: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
),
);
},
),
),
);
}
@override
void dispose() {
super.dispose();
subscription.cancel();
ipNotifier.dispose();
}
}

View File

@@ -1,233 +0,0 @@
import 'dart:async';
import 'package:dio/dio.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';
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
const NetworkDetection({super.key});
@override
State<NetworkDetection> createState() => _NetworkDetectionState();
}
class _NetworkDetectionState extends State<NetworkDetection> {
bool? _preIsStart;
Function? _checkIpDebounce;
Timer? _setTimeoutTimer;
CancelToken? cancelToken;
_checkIp() async {
final appState = globalState.appController.appState;
final appFlowingState = globalState.appController.appFlowingState;
final isInit = appState.isInit;
if (!isInit) return;
final isStart = appFlowingState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
_clearSetTimeoutTimer();
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
_preIsStart = isStart;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
cancelToken = CancelToken();
try {
final ipInfo = await request.checkIp(cancelToken: cancelToken);
if (ipInfo != null) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
return;
}
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (e) {
if (e.toString() == "cancelled") {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
}
}
_clearSetTimeoutTimer() {
if (_setTimeoutTimer != null) {
_setTimeoutTimer?.cancel();
_setTimeoutTimer = null;
}
}
_checkIpContainer(Widget child) {
return Selector<AppState, num>(
selector: (_, appState) {
return appState.checkIpNum;
},
builder: (_, checkIpNum, child) {
if (_checkIpDebounce != null) {
_checkIpDebounce!();
}
return child!;
},
child: child,
);
}
@override
dispose() {
super.dispose();
}
String countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
return countryCode;
}
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
}
@override
Widget build(BuildContext context) {
_checkIpDebounce ??= debounce(_checkIp);
return _checkIpContainer(
ValueListenableBuilder<NetworkDetectionState>(
valueListenable: networkDetectionState,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isTesting = state.isTesting;
return CommonCard(
onPressed: () {},
child: Column(
children: [
Flexible(
flex: 0,
child: Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.network_check,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: FadeBox(
child: isTesting
? Text(
appLocalizations.checking,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.titleMedium,
)
: ipInfo != null
? Container(
alignment: Alignment.centerLeft,
height: globalState
.measure.titleMediumHeight,
child: Text(
countryCodeToEmoji(
ipInfo.countryCode),
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontFamily:
FontFamily.twEmoji.value,
),
),
)
: Text(
appLocalizations.checkError,
style: Theme.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
),
Container(
height: globalState.measure.titleLargeHeight + 24 - 2,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox(
child: ipInfo != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.titleLarge
?.toSoftBold.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
)
: FadeBox(
child: isTesting == false && ipInfo == null
? Text(
"timeout",
style: context.textTheme.titleLarge
?.copyWith(color: Colors.red)
.toSoftBold
.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
),
),
),
)
],
),
);
},
),
);
}
}

View File

@@ -1,166 +0,0 @@
import 'package:fl_clash/common/common.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 NetworkSpeed extends StatefulWidget {
const NetworkSpeed({super.key});
@override
State<NetworkSpeed> createState() => _NetworkSpeedState();
}
class _NetworkSpeedState extends State<NetworkSpeed> {
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
List<Point> _getPoints(List<Traffic> traffics) {
List<Point> trafficPoints = traffics
.toList()
.asMap()
.map(
(index, e) => MapEntry(
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
.values
.toList();
return [...initPoints, ...trafficPoints];
}
Traffic _getLastTraffic(List<Traffic> traffics) {
if (traffics.isEmpty) return Traffic();
return traffics.last;
}
Widget _getLabel({
required String label,
required IconData iconData,
required TrafficValue value,
}) {
final showValue = value.showValue;
final showUnit = "${value.showUnit}/s";
final titleLargeSoftBold =
Theme.of(context).textTheme.titleLarge?.toSoftBold;
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight;
final valueText = Text(
showValue,
style: titleLargeSoftBold,
maxLines: 1,
);
final unitText = Text(
showUnit,
style: bodyMedium,
maxLines: 1,
);
final size = globalState.measure.computeTextSize(valueText);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: Icon(iconData),
),
Flexible(
child: Text(
label,
style: Theme.of(context).textTheme.titleSmall?.toSoftBold,
),
),
],
),
SizedBox(
width: size.width,
height: size.height,
child: OverflowBox(
maxWidth: 156,
alignment: Alignment.centerLeft,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: valueText,
),
const Flexible(
flex: 0,
child: SizedBox(
width: 4,
),
),
Flexible(
child: unitText,
),
],
),
))
],
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp,
),
child: Selector<AppFlowingState, List<Traffic>>(
selector: (_, appFlowingState) => appFlowingState.traffics,
builder: (_, traffics, __) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 0,
child: LineChart(
color: Theme.of(context).colorScheme.primary,
points: _getPoints(traffics),
height: 100,
),
),
const Flexible(child: SizedBox(height: 16)),
Flexible(
flex: 0,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: _getLabel(
iconData: Icons.upload,
label: appLocalizations.upload,
value: _getLastTraffic(traffics).up,
),
),
Expanded(
child: _getLabel(
iconData: Icons.download,
label: appLocalizations.download,
value: _getLastTraffic(traffics).down,
),
),
],
),
)
],
),
);
},
),
);
}
}

View File

@@ -1,69 +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/widgets.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class OutboundMode extends StatelessWidget {
const OutboundMode({super.key});
_changeMode(BuildContext context, Mode? value) async {
final appController = globalState.appController;
final clashConfig = appController.clashConfig;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
appController.addCheckIpNumDebounce();
}
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, Mode>(
selector: (_, clashConfig) => clashConfig.mode,
builder: (_, mode, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final item in Mode.values)
ListItem.radio(
horizontalTitleGap: 4,
prue: true,
padding: const EdgeInsets.only(
left: 12,
right: 16,
top: 8,
bottom: 8,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
_changeMode(context, value);
},
),
title: Text(
Intl.message(item.name),
style:
Theme.of(context).textTheme.titleMedium?.toSoftBold,
),
),
],
),
),
);
},
);
}
}

View File

@@ -1,131 +0,0 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/system.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 '../config/network.dart';
class TUNButton extends StatelessWidget {
const TUNButton({super.key});
@override
Widget build(BuildContext context) {
return ButtonContainer(
onPressed: () {
showSheet(
context: context,
builder: (_) {
return generateListView(generateSection(
items: [
if (system.isDesktop) const TUNItem(),
const TunStackItem(),
],
));
},
title: appLocalizations.tun,
);
},
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return LocaleBuilder(
builder: (_) => Switch(
value: enable,
onChanged: (value) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
),
);
},
),
);
}
}
class SystemProxyButton extends StatelessWidget {
const SystemProxyButton({super.key});
@override
Widget build(BuildContext context) {
return ButtonContainer(
onPressed: () {
showSheet(
context: context,
builder: (_) {
return generateListView(
generateSection(
items: [
SystemProxyItem(),
BypassDomainItem(),
],
),
);
},
title: appLocalizations.systemProxy,
);
},
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
builder: (_, systemProxy, __) {
return LocaleBuilder(
builder: (_) => Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.networkProps =
config.networkProps.copyWith(systemProxy: value);
},
),
);
},
),
);
}
}
class ButtonContainer extends StatelessWidget {
final Info info;
final Widget child;
final VoidCallback onPressed;
const ButtonContainer({
super.key,
required this.info,
required this.child,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: onPressed,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoHeader(
info: info,
actions: [
child,
],
),
],
),
);
}
}

View File

@@ -1,95 +0,0 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key});
Widget getTrafficDataItem(
BuildContext context,
IconData iconData,
TrafficValue trafficValue,
) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
iconData,
size: 18,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: Text(
trafficValue.showValue,
style: context.textTheme.labelLarge?.copyWith(fontSize: 18),
maxLines: 1,
),
),
],
),
),
Text(
trafficValue.showUnit,
style: context.textTheme.labelMedium?.toLight,
),
],
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
child: Selector<AppFlowingState, Traffic>(
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
builder: (_, totalTraffic, __) {
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;
return Padding(
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: getTrafficDataItem(
context,
Icons.arrow_upward,
upTotalTrafficValue,
),
),
const SizedBox(
height: 4,
),
Flexible(
flex: 1,
child: getTrafficDataItem(
context,
Icons.arrow_downward,
downTotalTrafficValue,
),
),
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.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 IntranetIP extends StatelessWidget {
const IntranetIP({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
info: Info(
label: appLocalizations.intranetIP,
iconData: Icons.devices,
),
onPressed: () {},
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: Selector<AppFlowingState, String?>(
selector: (_, appFlowingState) => appFlowingState.localIp,
builder: (_, value, __) {
return FadeBox(
child: value != null
? TooltipText(
text: Text(
value.isNotEmpty
? value
: appLocalizations.noNetwork,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
: Container(
padding: EdgeInsets.all(2),
child: AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
),
);
},
),
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'dart:async';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
final _memoryInfoStateNotifier =
ValueNotifier<TrafficValue>(TrafficValue(value: 0));
class MemoryInfo extends StatefulWidget {
const MemoryInfo({super.key});
@override
State<MemoryInfo> createState() => _MemoryInfoState();
}
class _MemoryInfoState extends State<MemoryInfo> {
Timer? timer;
@override
void initState() {
super.initState();
clashCore.getMemory().then((memory) {
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
});
_updateMemoryData();
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
_updateMemoryData() {
timer = Timer(Duration(seconds: 2), () async {
final memory = await clashCore.getMemory();
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
_updateMemoryData();
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
info: Info(
iconData: Icons.memory,
label: appLocalizations.memoryInfo,
),
onPressed: () {
clashCore.requestGc();
},
child: ValueListenableBuilder(
valueListenable: _memoryInfoStateNotifier,
builder: (_, trafficValue, __) {
return Column(
children: [
Padding(
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
top: 12,
),
child: Row(
children: [
Text(
trafficValue.showValue,
style: context.textTheme.titleLarge?.toLight,
),
SizedBox(
width: 8,
),
Text(
trafficValue.showUnit,
style: context.textTheme.titleLarge?.toLight,
)
],
),
),
Flexible(
child: Stack(
children: [
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.35,
waveColor: context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.toLighter,
),
),
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.9,
waveColor: context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1),
),
),
],
),
)
],
);
},
),
),
);
}
}

View File

@@ -0,0 +1,250 @@
import 'dart:async';
import 'package:dio/dio.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';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
const NetworkDetection({super.key});
@override
State<NetworkDetection> createState() => _NetworkDetectionState();
}
class _NetworkDetectionState extends State<NetworkDetection> {
bool? _preIsStart;
Timer? _setTimeoutTimer;
CancelToken? cancelToken;
Completer? checkedCompleter;
@override
void initState() {
super.initState();
}
_startCheck() async {
await checkedCompleter?.future;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
debouncer.call(
DebounceTag.checkIp,
_checkIp,
);
}
_checkIp() async {
final appState = globalState.appController.appState;
final appFlowingState = globalState.appController.appFlowingState;
final isInit = appState.isInit;
if (!isInit) return;
final isStart = appFlowingState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
_clearSetTimeoutTimer();
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
_preIsStart = isStart;
if (cancelToken != null) {
cancelToken!.cancel();
cancelToken = null;
}
cancelToken = CancelToken();
try {
final ipInfo = await request.checkIp(cancelToken: cancelToken);
if (ipInfo != null) {
checkedCompleter = Completer();
checkedCompleter?.complete(
Future.delayed(
Duration(milliseconds: 3000),
),
);
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
return;
}
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (e) {
if (e.toString() == "cancelled") {
_networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
}
}
@override
void dispose() {
_clearSetTimeoutTimer();
super.dispose();
}
_clearSetTimeoutTimer() {
if (_setTimeoutTimer != null) {
_setTimeoutTimer?.cancel();
_setTimeoutTimer = null;
}
}
_checkIpContainer(Widget child) {
return Selector<AppState, num>(
selector: (_, appState) {
return appState.checkIpNum;
},
shouldRebuild: (prev, next) {
if (prev != next) {
_startCheck();
}
return prev != next;
},
builder: (_, checkIpNum, child) {
return child!;
},
child: child,
);
}
_countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase();
if (code.length != 2) {
return countryCode;
}
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: _checkIpContainer(
ValueListenableBuilder<NetworkDetectionState>(
valueListenable: _networkDetectionState,
builder: (_, state, __) {
final ipInfo = state.ipInfo;
final isTesting = state.isTesting;
return CommonCard(
onPressed: () {},
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: globalState.measure.titleMediumHeight + 16,
padding: baseInfoEdgeInsets.copyWith(
bottom: 0,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
ipInfo != null
? Text(
_countryCodeToEmoji(
ipInfo.countryCode,
),
style: Theme.of(context)
.textTheme
.titleMedium
?.toLight
.copyWith(
fontFamily: FontFamily.twEmoji.value,
),
)
: Icon(
Icons.network_check,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.networkDetection,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
Container(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: SizedBox(
height: globalState.measure.bodyMediumHeight + 2,
child: FadeBox(
child: ipInfo != null
? TooltipText(
text: Text(
ipInfo.ip,
style: context.textTheme.bodyMedium?.toLight
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
: FadeBox(
child: isTesting == false && ipInfo == null
? Text(
"timeout",
style: context.textTheme.bodyMedium
?.copyWith(color: Colors.red)
.adjustSize(1),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Container(
padding: const EdgeInsets.all(2),
child: const AspectRatio(
aspectRatio: 1,
child: CircularProgressIndicator(),
),
),
),
),
),
)
],
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class NetworkSpeed extends StatefulWidget {
const NetworkSpeed({super.key});
@override
State<NetworkSpeed> createState() => _NetworkSpeedState();
}
class _NetworkSpeedState extends State<NetworkSpeed> {
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
List<Point> _getPoints(List<Traffic> traffics) {
List<Point> trafficPoints = traffics
.toList()
.asMap()
.map(
(index, e) => MapEntry(
index,
Point(
(index + initPoints.length).toDouble(),
e.speed.toDouble(),
),
),
)
.values
.toList();
return [...initPoints, ...trafficPoints];
}
Traffic _getLastTraffic(List<Traffic> traffics) {
if (traffics.isEmpty) return Traffic();
return traffics.last;
}
@override
Widget build(BuildContext context) {
final color = context.colorScheme.onSurfaceVariant.toLight;
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp,
),
child: Selector<AppFlowingState, List<Traffic>>(
selector: (_, appFlowingState) => appFlowingState.traffics,
builder: (_, traffics, __) {
return Stack(
children: [
Positioned.fill(
child: Padding(
padding: EdgeInsets.all(16).copyWith(
bottom: 0,
left: 0,
right: 0,
),
child: LineChart(
gradient: true,
color: Theme.of(context).colorScheme.primary,
points: _getPoints(traffics),
),
),
),
Positioned(
top: 0,
right: 0,
child: Transform.translate(
offset: Offset(
-16,
-20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
Icons.arrow_upward,
color: color,
size: 16,
),
SizedBox(
width: 2,
),
Text(
"${_getLastTraffic(traffics).up}/s",
style: context.textTheme.bodySmall?.copyWith(
color: color,
),
),
SizedBox(
width: 16,
),
Icon(
Icons.arrow_downward,
color: color,
size: 16,
),
SizedBox(
width: 2,
),
Text(
"${_getLastTraffic(traffics).down}/s",
style: context.textTheme.bodySmall?.copyWith(
color: color,
),
),
],
),
),
),
],
);
},
),
),
);
}
}

View File

@@ -0,0 +1,73 @@
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 OutboundMode extends StatelessWidget {
const OutboundMode({super.key});
@override
Widget build(BuildContext context) {
final height = getWidgetHeight(2);
return SizedBox(
height: height,
child: Selector<ClashConfig, Mode>(
selector: (_, clashConfig) => clashConfig.mode,
builder: (_, mode, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
bottom: 16,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final item in Mode.values)
Flexible(
child: ListItem.radio(
prue: true,
horizontalTitleGap: 4,
padding: const EdgeInsets.only(
left: 12,
right: 16,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.bodyMedium
?.toSoftBold,
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,156 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/network.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 TUNButton extends StatelessWidget {
const TUNButton({super.key});
@override
Widget build(BuildContext context) {
return LocaleBuilder(
builder: (_) => SizedBox(
height: getWidgetHeight(1),
child: CommonCard(
onPressed: () {
showSheet(
context: context,
body: generateListView(generateSection(
items: [
if (system.isDesktop) const TUNItem(),
const TunStackItem(),
],
)),
title: appLocalizations.tun,
);
},
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, enable, __) {
return Switch(
value: enable,
onChanged: (value) {
final clashConfig =
globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
);
},
)
],
),
),
),
),
);
}
}
class SystemProxyButton extends StatelessWidget {
const SystemProxyButton({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: getWidgetHeight(1),
child: LocaleBuilder(
builder: (_) => CommonCard(
onPressed: () {
showSheet(
context: context,
body: generateListView(
generateSection(
items: [
SystemProxyItem(),
BypassDomainItem(),
],
),
),
title: appLocalizations.systemProxy,
);
},
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
),
),
Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
builder: (_, systemProxy, __) {
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.networkProps =
config.networkProps.copyWith(systemProxy: value);
},
);
},
)
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,220 @@
import 'dart:math';
import 'package:fl_clash/common/common.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 TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key});
Widget getTrafficDataItem(
BuildContext context,
Icon icon,
TrafficValue trafficValue,
) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
icon,
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: Text(
trafficValue.showValue,
style: context.textTheme.bodySmall,
maxLines: 1,
),
),
],
),
),
Text(
trafficValue.showUnit,
style: context.textTheme.bodySmall?.toLighter,
),
],
);
}
@override
Widget build(BuildContext context) {
final primaryColor =
context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2);
final secondaryColor =
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3);
return SizedBox(
height: getWidgetHeight(2),
child: CommonCard(
info: Info(
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
onPressed: () {},
child: Selector<AppFlowingState, Traffic>(
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
builder: (_, totalTraffic, __) {
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;
return Padding(
padding: baseInfoEdgeInsets.copyWith(
top: 0,
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Container(
padding: EdgeInsets.symmetric(
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AspectRatio(
aspectRatio: 1,
child: DonutChart(
data: [
DonutChartData(
value: upTotalTrafficValue.value.toDouble(),
color: primaryColor,
),
DonutChartData(
value: downTotalTrafficValue.value.toDouble(),
color: secondaryColor,
),
],
),
),
SizedBox(
width: 8,
),
Flexible(
child: LayoutBuilder(
builder: (_, container) {
final uploadText = Text(
maxLines: 1,
appLocalizations.upload,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
);
final downloadText = Text(
maxLines: 1,
appLocalizations.download,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
);
final uploadTextSize = globalState.measure
.computeTextSize(uploadText);
final downloadTextSize = globalState.measure
.computeTextSize(downloadText);
final maxTextWidth = max(uploadTextSize.width,
downloadTextSize.width);
if (maxTextWidth + 24 > container.maxWidth) {
return Container();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 20,
height: 8,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(2),
),
),
SizedBox(
width: 4,
),
Text(
maxLines: 1,
appLocalizations.upload,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
),
],
),
SizedBox(
height: 4,
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 20,
height: 8,
decoration: BoxDecoration(
color: secondaryColor,
borderRadius:
BorderRadius.circular(2),
),
),
SizedBox(
width: 4,
),
Text(
maxLines: 1,
appLocalizations.download,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall,
),
],
),
],
);
},
),
),
],
),
),
),
getTrafficDataItem(
context,
Icon(
Icons.arrow_upward,
color: primaryColor,
size: 14,
),
upTotalTrafficValue,
),
const SizedBox(
height: 8,
),
getTrafficDataItem(
context,
Icon(
Icons.arrow_downward,
color: secondaryColor,
size: 14,
),
downTotalTrafficValue,
)
],
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,7 @@
export 'intranet_ip.dart';
export 'network_detection.dart';
export 'network_speed.dart';
export 'outbound_mode.dart';
export 'quick_options.dart';
export 'traffic_usage.dart';
export 'memory_info.dart';

View File

@@ -49,11 +49,11 @@ class _LogsFragmentState extends State<LogsFragment> {
@override
void dispose() {
super.dispose();
timer?.cancel();
logsNotifier.dispose();
scrollController.dispose();
timer = null;
super.dispose();
}
_handleExport() async {
@@ -87,9 +87,6 @@ class _LogsFragmentState extends State<LogsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
_handleExport();
@@ -235,8 +232,8 @@ class LogsSearchDelegate extends SearchDelegate {
@override
void dispose() {
super.dispose();
logsNotifier.dispose();
super.dispose();
}
get state => logsNotifier.value;

View File

@@ -52,7 +52,7 @@ class _EditProfileState extends State<EditProfile> {
_handleConfirm() async {
if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>();
var profile = widget.profile.copyWith(
final profile = widget.profile.copyWith(
url: urlController.text,
label: labelController.text,
autoUpdate: autoUpdate,

View File

@@ -52,9 +52,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
try {
await appController.updateProfile(profile);
if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfileDebounce();
}
} catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile(
@@ -93,16 +90,13 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
},
icon: const Icon(Icons.sync),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
final profiles = globalState.appController.config.profiles;
showSheet(
title: appLocalizations.profilesSort,
context: context,
builder: (_) => SizedBox(
body: SizedBox(
height: 400,
child: ReorderableProfiles(profiles: profiles),
),
@@ -213,7 +207,7 @@ class ProfileItem extends StatelessWidget {
final appController = globalState.appController;
final config = appController.config;
if (profile.type == ProfileType.file) return;
await globalState.safeRun(() async {
await globalState.safeRun(silence: false, () async {
try {
config.setProfile(
profile.copyWith(
@@ -221,9 +215,6 @@ class ProfileItem extends StatelessWidget {
),
);
await appController.updateProfile(profile);
if (profile.id == appController.config.currentProfile?.id) {
appController.applyProfileDebounce();
}
} catch (e) {
config.setProfile(
profile.copyWith(
@@ -296,6 +287,7 @@ class ProfileItem extends StatelessWidget {
child: CircularProgressIndicator(),
)
: CommonPopupMenu<ProfileActions>(
icon: Icon(Icons.more_vert),
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,

View File

@@ -41,9 +41,9 @@ class _ViewProfileState extends State<ViewProfile> {
@override
void dispose() {
super.dispose();
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
Profile get profile => widget.profile;

View File

@@ -11,7 +11,6 @@ class ProxyCard extends StatelessWidget {
final String groupName;
final Proxy proxy;
final GroupType groupType;
final CommonCardType style;
final ProxyCardType type;
const ProxyCard({
@@ -19,7 +18,6 @@ class ProxyCard extends StatelessWidget {
required this.groupName,
required this.proxy,
required this.groupType,
this.style = CommonCardType.plain,
required this.type,
});
@@ -115,15 +113,11 @@ class ProxyCard extends StatelessWidget {
groupName,
nextProxyName,
);
await appController.changeProxyDebounce([
groupName,
nextProxyName,
]);
await appController.changeProxyDebounce(groupName, nextProxyName);
return;
}
globalState.showSnackBar(
context,
message: appLocalizations.notSelectedTip,
globalState.showNotifier(
appLocalizations.notSelectedTip,
);
}
@@ -138,7 +132,6 @@ class ProxyCard extends StatelessWidget {
return Stack(
children: [
CommonCard(
type: style,
key: key,
onPressed: () {
_changeProxy(context);
@@ -167,8 +160,8 @@ class ProxyCard extends StatelessWidget {
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color
?.toLight(),
color:
context.textTheme.bodySmall?.color?.toLight,
),
);
},
@@ -192,8 +185,8 @@ class ProxyCard extends StatelessWidget {
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context.textTheme.bodySmall?.color
?.toLight(),
color: context
.textTheme.bodySmall?.color?.toLight,
),
),
),

View File

@@ -65,10 +65,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
@override
void dispose() {
super.dispose();
_headerStateNotifier.dispose();
_controller.removeListener(_adjustHeader);
_controller.dispose();
super.dispose();
}
_handleChange(Set<String> currentUnfoldSet, String groupName) {
@@ -299,7 +299,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
headerState.currentIndex > state.groupNames.length - 1
? 0
: headerState.currentIndex;
if (index < 0) {
if (index < 0 || state.groupNames.isEmpty) {
return Container();
}
return Stack(
@@ -442,10 +442,10 @@ class _ListHeaderState extends State<ListHeader>
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
child: CommonIcon(
child: CommonTargetIcon(
src: icon,
size: 32,
),
@@ -454,7 +454,7 @@ class _ListHeaderState extends State<ListHeader>
margin: const EdgeInsets.only(
right: 16,
),
child: CommonIcon(
child: CommonTargetIcon(
src: icon,
size: 42,
),
@@ -471,7 +471,10 @@ class _ListHeaderState extends State<ListHeader>
Widget build(BuildContext context) {
return CommonCard(
key: widget.key,
radius: 18,
backgroundColor: WidgetStatePropertyAll(
context.colorScheme.surfaceContainer,
),
radius: 14,
type: CommonCardType.filled,
child: Container(
padding: const EdgeInsets.symmetric(

View File

@@ -61,7 +61,7 @@ class _ProvidersState extends State<Providers> {
},
);
await Future.wait(updateProviders);
await globalState.appController.updateGroupDebounce();
await globalState.appController.updateGroupsDebounce();
}
@override
@@ -125,7 +125,7 @@ class ProviderItem extends StatelessWidget {
await clashCore.getExternalProvider(provider.name),
);
});
await globalState.appController.updateGroupDebounce();
await globalState.appController.updateGroupsDebounce();
}
_handleSideLoadProvider() async {
@@ -147,7 +147,7 @@ class ProviderItem extends StatelessWidget {
);
if (message.isNotEmpty) throw message;
});
await globalState.appController.updateGroupDebounce();
await globalState.appController.updateGroupsDebounce();
}
String _buildProviderDesc() {

View File

@@ -40,9 +40,6 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
Icons.poll_outlined,
),
),
const SizedBox(
width: 8,
),
],
if (proxiesType == ProxiesType.tab) ...[
IconButton(
@@ -53,9 +50,6 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
Icons.adjust_outlined,
),
),
const SizedBox(
width: 8,
)
] else ...[
IconButton(
onPressed: () {
@@ -85,7 +79,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonIcon(
child: CommonTargetIcon(
src: item.value,
size: 42,
),
@@ -110,18 +104,13 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
Icons.style_outlined,
),
),
const SizedBox(
width: 8,
)
],
IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
builder: (context) {
return const ProxiesSetting();
},
body: const ProxiesSetting(),
);
},
icon: const Icon(

View File

@@ -30,8 +30,8 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
@override
void dispose() {
super.dispose();
_destroyTabController();
super.dispose();
}
scrollToGroupSelected() {
@@ -62,49 +62,46 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
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,
);
Navigator.of(context).pop();
},
isSelected: groupName == state.currentGroupName,
)
],
),
);
},
),
);
},
body: 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,
);
Navigator.of(context).pop();
},
isSelected: groupName == state.currentGroupName,
)
],
),
);
},
),
),
title: appLocalizations.proxyGroup,
);
}
@@ -117,9 +114,11 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
}
final currentGroup = currentGroups[index ?? _tabController!.index];
currentProxies = currentGroup.all;
appController.config.updateCurrentGroupName(
currentGroup.name,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
appController.config.updateCurrentGroupName(
currentGroup.name,
);
});
}
_destroyTabController() {
@@ -129,6 +128,10 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
}
_updateTabController(int length, int index) {
if (length == 0) {
_destroyTabController();
return;
}
final realIndex = index == -1 ? 0 : index;
_tabController ??= TabController(
length: length,
@@ -162,6 +165,9 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
(item) => item == state.currentGroupName,
);
_updateTabController(state.groupNames.length, index);
if (state.groupNames.isEmpty) {
return Container();
}
final GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
@@ -273,20 +279,23 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
@override
void dispose() {
super.dispose();
_controller.dispose();
super.dispose();
}
scrollToSelected() {
if (_controller.position.maxScrollExtent == 0) {
return;
}
final sortedProxies = globalState.appController.getSortProxies(
currentProxies,
);
_controller.animateTo(
min(
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: currentProxies,
proxies: sortedProxies,
),
_controller.position.maxScrollExtent,
),

View File

@@ -95,10 +95,10 @@ class _RequestsFragmentState extends State<RequestsFragment> {
@override
void dispose() {
super.dispose();
timer?.cancel();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override

View File

@@ -332,5 +332,6 @@
"routeAddress": "Route address",
"routeAddressDesc": "Config listen route address",
"pleaseInputAdminPassword": "Please enter the admin password",
"copyEnvVar": "Copying environment variables"
"copyEnvVar": "Copying environment variables",
"memoryInfo": "Memory info"
}

View File

@@ -332,5 +332,6 @@
"routeAddress": "路由地址",
"routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量"
"copyEnvVar": "复制环境变量",
"memoryInfo": "内存信息"
}

View File

@@ -259,6 +259,7 @@ class MessageLookup extends MessageLookupByLibrary {
"loopbackDesc": MessageLookupByLibrary.simpleMessage(
"Used for UWP loopback unlocking"),
"loose": MessageLookupByLibrary.simpleMessage("Loose"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("Memory info"),
"min": MessageLookupByLibrary.simpleMessage("Min"),
"minimizeOnExit":
MessageLookupByLibrary.simpleMessage("Minimize on exit"),

View File

@@ -205,6 +205,7 @@ class MessageLookup extends MessageLookupByLibrary {
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
"min": MessageLookupByLibrary.simpleMessage("最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
"minimizeOnExitDesc":

View File

@@ -3389,6 +3389,16 @@ class AppLocalizations {
args: [],
);
}
/// `Memory info`
String get memoryInfo {
return Intl.message(
'Memory info',
name: 'memoryInfo',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -1,4 +1,5 @@
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';
@@ -20,8 +21,6 @@ class ClashManager extends StatefulWidget {
}
class _ClashContainerState extends State<ClashManager> with AppMessageListener {
Function? updateDelayDebounce;
Widget _updateContainer(Widget child) {
return Selector2<Config, ClashConfig, ClashConfigState>(
selector: (_, config, clashConfig) => ClashConfigState(
@@ -103,18 +102,21 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
final appController = globalState.appController;
appController.setDelay(delay);
super.onDelay(delay);
updateDelayDebounce ??= debounce(() async {
await appController.updateGroupDebounce();
await appController.addCheckIpNumDebounce();
}, milliseconds: 5000);
updateDelayDebounce!();
debouncer.call(
DebounceTag.updateDelay,
() async {
await appController.updateGroupsDebounce();
// await appController.addCheckIpNumDebounce();
},
duration: const Duration(milliseconds: 5000),
);
}
@override
void onLog(Log log) {
globalState.appController.appFlowingState.addLog(log);
if (log.logLevel == LogLevel.error) {
globalState.appController.showSnackBar(log.payload ?? '');
globalState.showNotifier(log.payload ?? '');
}
super.onLog(log);
}
@@ -139,7 +141,7 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
providerName,
),
);
await appController.updateGroupDebounce();
await appController.updateGroupsDebounce();
super.onLoaded(providerName);
}
}

View File

@@ -0,0 +1,43 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
class ConnectivityManager extends StatefulWidget {
final VoidCallback? onConnectivityChanged;
final Widget child;
const ConnectivityManager({
super.key,
this.onConnectivityChanged,
required this.child,
});
@override
State<ConnectivityManager> createState() => _ConnectivityManagerState();
}
class _ConnectivityManagerState extends State<ConnectivityManager> {
late StreamSubscription subscription;
@override
void initState() {
super.initState();
subscription = Connectivity().onConnectivityChanged.listen((_) async {
if (widget.onConnectivityChanged != null) {
widget.onConnectivityChanged!();
}
});
}
@override
void dispose() {
subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}

View File

@@ -6,4 +6,6 @@ export 'tile_manager.dart';
export 'app_state_manager.dart';
export 'vpn_manager.dart';
export 'media_manager.dart';
export 'proxy_manager.dart';
export 'proxy_manager.dart';
export 'connectivity_manager.dart';
export 'message_manager.dart';

View File

@@ -0,0 +1,330 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
class MessageManager extends StatefulWidget {
final Widget child;
const MessageManager({
super.key,
required this.child,
});
@override
State<MessageManager> createState() => MessageManagerState();
}
class MessageManagerState extends State<MessageManager>
with SingleTickerProviderStateMixin {
final _floatMessageKey = GlobalKey();
List<CommonMessage> bufferMessages = [];
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
final _floatMessageNotifier = ValueNotifier<CommonMessage?>(null);
double maxWidth = 0;
late AnimationController _animationController;
Completer? _animationCompleter;
late Animation<Offset> _floatOffsetAnimation;
late Animation<Offset> _commonOffsetAnimation;
final animationDuration = commonDuration * 2;
_initTransformState() {
_floatMessageNotifier.value = null;
_floatOffsetAnimation = Tween(
begin: Offset.zero,
end: Offset.zero,
).animate(_animationController);
_commonOffsetAnimation = _floatOffsetAnimation = Tween(
begin: Offset.zero,
end: Offset.zero,
).animate(_animationController);
}
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 200),
);
_initTransformState();
}
@override
void dispose() {
_messagesNotifier.dispose();
_floatMessageNotifier.dispose();
_animationController.dispose();
super.dispose();
}
message(String text) async {
final commonMessage = CommonMessage(
id: other.uuidV4,
text: text,
);
bufferMessages.add(commonMessage);
await _animationCompleter?.future;
_showMessage();
}
_showMessage() {
final commonMessage = bufferMessages.removeAt(0);
_floatOffsetAnimation = Tween(
begin: Offset(-maxWidth, 0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
0.5,
1,
curve: Curves.easeInOut,
),
),
);
_floatMessageNotifier.value = commonMessage;
WidgetsBinding.instance.addPostFrameCallback((_) async {
final size = _floatMessageKey.currentContext?.size ?? Size.zero;
_commonOffsetAnimation = Tween(
begin: Offset.zero,
end: Offset(0, -size.height - 12),
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
0,
0.7,
curve: Curves.easeInOut,
),
),
);
_animationCompleter = Completer();
_animationCompleter?.complete(_animationController.forward(from: 0));
await _animationCompleter?.future;
_initTransformState();
_messagesNotifier.value = List.from(_messagesNotifier.value)
..add(commonMessage);
Future.delayed(
commonMessage.duration,
() {
_removeMessage(commonMessage);
},
);
});
}
Widget _wrapOffset(Widget child) {
return AnimatedBuilder(
animation: _animationController.view,
builder: (context, child) {
return Transform.translate(
offset: _commonOffsetAnimation.value,
child: child!,
);
},
child: child,
);
}
Widget _wrapMessage(CommonMessage message) {
return Material(
elevation: 2,
borderRadius: BorderRadius.circular(8),
color: context.colorScheme.secondaryFixedDim,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 15),
child: Text(
message.text,
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSecondaryFixedVariant,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
),
);
}
Widget _floatMessage() {
return ValueListenableBuilder(
valueListenable: _floatMessageNotifier,
builder: (_, message, ___) {
if (message == null) {
return SizedBox();
}
return AnimatedBuilder(
key: _floatMessageKey,
animation: _animationController.view,
builder: (_, child) {
if (!_animationController.isAnimating) {
return Opacity(
opacity: 0,
child: child,
);
}
return Transform.translate(
offset: _floatOffsetAnimation.value,
child: child,
);
},
child: _wrapMessage(
message,
),
);
},
);
}
_removeMessage(CommonMessage commonMessage) async {
final itemWrapState = GlobalObjectKey(commonMessage.id).currentState
as _MessageItemWrapState?;
await itemWrapState?.transform(
Offset(-maxWidth, 0),
);
_messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value)
..remove(commonMessage);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
LayoutBuilder(
builder: (context, container) {
maxWidth = container.maxWidth / 2 + 16;
return SizedBox(
width: maxWidth,
child: ValueListenableBuilder(
valueListenable: globalState.safeMessageOffsetNotifier,
builder: (_, offset, child) {
if (offset == Offset.zero) {
return SizedBox();
}
return Transform.translate(
offset: offset,
child: child!,
);
},
child: Container(
padding: EdgeInsets.only(
right: 0,
left: 8,
top: 0,
bottom: 16,
),
alignment: Alignment.bottomLeft,
child: Stack(
alignment: Alignment.bottomLeft,
children: [
SingleChildScrollView(
reverse: true,
physics: NeverScrollableScrollPhysics(),
child: ValueListenableBuilder(
valueListenable: _messagesNotifier,
builder: (_, messages, ___) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final message in messages) ...[
if (message != messages.last)
SizedBox(
height: 8,
),
_MessageItemWrap(
key: GlobalObjectKey(message.id),
child: _wrapOffset(
_wrapMessage(message),
),
),
],
],
);
},
),
),
_floatMessage(),
],
),
),
),
);
},
)
],
);
}
}
class _MessageItemWrap extends StatefulWidget {
final Widget child;
const _MessageItemWrap({
super.key,
required this.child,
});
@override
State<_MessageItemWrap> createState() => _MessageItemWrapState();
}
class _MessageItemWrapState extends State<_MessageItemWrap>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Offset _nextOffset = Offset.zero;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: commonDuration * 1.5,
);
}
transform(Offset offset) async {
_nextOffset = offset;
await _controller.forward(from: 0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
if (_nextOffset == Offset.zero) {
return child!;
}
final offset = Tween(
begin: Offset.zero,
end: _nextOffset,
)
.animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
),
)
.value;
return Transform.translate(
offset: offset,
child: child!,
);
},
child: widget.child,
);
}
}

View File

@@ -1,11 +1,10 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../common/function.dart';
class VpnManager extends StatefulWidget {
final Widget child;
@@ -19,21 +18,20 @@ class VpnManager extends StatefulWidget {
}
class _VpnContainerState extends State<VpnManager> {
Function? vpnTipDebounce;
showTip() {
vpnTipDebounce ??= debounce<Function()>(() async {
WidgetsBinding.instance.addPostFrameCallback((_) {
final appFlowingState = globalState.appController.appFlowingState;
if (appFlowingState.isStart) {
globalState.showSnackBar(
context,
message: appLocalizations.vpnTip,
);
}
});
});
vpnTipDebounce!();
debouncer.call(
DebounceTag.vpnTip,
() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final appFlowingState = globalState.appController.appFlowingState;
if (appFlowingState.isStart) {
globalState.showNotifier(
appLocalizations.vpnTip,
);
}
});
},
);
}
@override

View File

@@ -1,6 +1,7 @@
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/state.dart';
import 'package:flutter/material.dart';
@@ -25,15 +26,20 @@ class _WindowContainerState extends State<WindowManager>
Function? updateLaunchDebounce;
_autoLaunchContainer(Widget child) {
return Selector<Config, AutoLaunchState>(
selector: (_, config) => AutoLaunchState(
isAutoLaunch: config.appSetting.autoLaunch,
),
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoLaunch,
shouldRebuild: (prev, next) {
if (prev != next) {
debouncer.call(
DebounceTag.autoLaunch,
() {
autoLaunch?.updateStatus(next);
},
);
}
return prev != next;
},
builder: (_, state, child) {
updateLaunchDebounce ??= debounce((AutoLaunchState state) {
autoLaunch?.updateStatus(state);
});
updateLaunchDebounce!([state]);
return child!;
},
child: child,
@@ -67,6 +73,12 @@ class _WindowContainerState extends State<WindowManager>
@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
@@ -163,9 +175,9 @@ class _WindowHeaderState extends State<WindowHeader> {
@override
void dispose() {
super.dispose();
isMaximizedNotifier.dispose();
isPinNotifier.dispose();
super.dispose();
}
_updateMaximized() {
@@ -255,7 +267,7 @@ class _WindowHeaderState extends State<WindowHeader> {
_updateMaximized();
},
child: Container(
color: context.colorScheme.secondary.toSoft(),
color: context.colorScheme.secondary.toSoft,
alignment: Alignment.centerLeft,
height: kHeaderHeight,
),

View File

@@ -212,9 +212,7 @@ class AppState with ChangeNotifier {
case Mode.direct:
return [];
case Mode.global:
return groups
.where((element) => element.name == GroupName.GLOBAL.name)
.toList();
return groups.toList();
case Mode.rule:
return groups
.where((item) => item.hidden == false)
@@ -308,6 +306,7 @@ class AppFlowingState with ChangeNotifier {
List<Log> _logs;
List<Traffic> _traffics;
Traffic _totalTraffic;
String? _localIp;
AppFlowingState()
: _logs = [],
@@ -352,7 +351,7 @@ class AppFlowingState with ChangeNotifier {
addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)..add(traffic);
const maxLength = 60;
const maxLength = 30;
_traffics = _traffics.safeSublist(_traffics.length - maxLength);
notifyListeners();
}
@@ -365,4 +364,13 @@ class AppFlowingState with ChangeNotifier {
notifyListeners();
}
}
String? get localIp => _localIp;
set localIp(String? value) {
if (_localIp != value) {
_localIp = value;
notifyListeners();
}
}
}

View File

@@ -180,7 +180,7 @@ class Traffic {
TrafficValue up;
TrafficValue down;
Traffic({num? up, num? down})
Traffic({int? up, int? down})
: id = DateTime.now().millisecondsSinceEpoch,
up = TrafficValue(value: up),
down = TrafficValue(value: down);
@@ -225,11 +225,11 @@ class TrafficValueShow {
@immutable
class TrafficValue {
final num _value;
final int _value;
const TrafficValue({num? value}) : _value = value ?? 0;
const TrafficValue({int? value}) : _value = value ?? 0;
num get value => _value;
int get value => _value;
String get show => "$showValue $showUnit";
@@ -343,7 +343,7 @@ class SystemColorSchemes {
);
}
return lightColorScheme != null
? ColorScheme.fromSeed(seedColor: darkColorScheme!.primary)
? ColorScheme.fromSeed(seedColor: lightColorScheme!.primary)
: ColorScheme.fromSeed(seedColor: defaultPrimaryColor);
}
}

View File

@@ -1,3 +1,5 @@
// ignore_for_file: invalid_annotation_target
import 'dart:io';
import 'package:fl_clash/common/common.dart';
@@ -8,16 +10,42 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'models.dart';
part 'generated/config.freezed.dart';
part 'generated/config.g.dart';
final defaultAppSetting = const AppSetting().copyWith(
isAnimateToPage: system.isDesktop ? false : true,
);
const List<DashboardWidget> defaultDashboardWidgets = [
DashboardWidget.networkSpeed,
DashboardWidget.systemProxyButton,
DashboardWidget.tunButton,
DashboardWidget.outboundMode,
DashboardWidget.networkDetection,
DashboardWidget.trafficUsage,
DashboardWidget.intranetIp,
];
List<DashboardWidget> dashboardWidgetsRealFormJson(
List<dynamic>? dashboardWidgets) {
try {
return dashboardWidgets
?.map((e) => $enumDecode(_$DashboardWidgetEnumMap, e))
.toList() ??
defaultDashboardWidgets;
} catch (_) {
return defaultDashboardWidgets;
}
}
@freezed
class AppSetting with _$AppSetting {
const factory AppSetting({
String? locale,
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
@Default(defaultDashboardWidgets)
List<DashboardWidget> dashboardWidgets,
@Default(false) bool onlyProxy,
@Default(false) bool autoLaunch,
@Default(false) bool silentLaunch,

View File

@@ -21,6 +21,9 @@ AppSetting _$AppSettingFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$AppSetting {
String? get locale => throw _privateConstructorUsedError;
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> get dashboardWidgets =>
throw _privateConstructorUsedError;
bool get onlyProxy => throw _privateConstructorUsedError;
bool get autoLaunch => throw _privateConstructorUsedError;
bool get silentLaunch => throw _privateConstructorUsedError;
@@ -53,6 +56,8 @@ abstract class $AppSettingCopyWith<$Res> {
@useResult
$Res call(
{String? locale,
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> dashboardWidgets,
bool onlyProxy,
bool autoLaunch,
bool silentLaunch,
@@ -84,6 +89,7 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
@override
$Res call({
Object? locale = freezed,
Object? dashboardWidgets = null,
Object? onlyProxy = null,
Object? autoLaunch = null,
Object? silentLaunch = null,
@@ -103,6 +109,10 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
? _value.locale
: locale // ignore: cast_nullable_to_non_nullable
as String?,
dashboardWidgets: null == dashboardWidgets
? _value.dashboardWidgets
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
as List<DashboardWidget>,
onlyProxy: null == onlyProxy
? _value.onlyProxy
: onlyProxy // ignore: cast_nullable_to_non_nullable
@@ -169,6 +179,8 @@ abstract class _$$AppSettingImplCopyWith<$Res>
@useResult
$Res call(
{String? locale,
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> dashboardWidgets,
bool onlyProxy,
bool autoLaunch,
bool silentLaunch,
@@ -198,6 +210,7 @@ class __$$AppSettingImplCopyWithImpl<$Res>
@override
$Res call({
Object? locale = freezed,
Object? dashboardWidgets = null,
Object? onlyProxy = null,
Object? autoLaunch = null,
Object? silentLaunch = null,
@@ -217,6 +230,10 @@ class __$$AppSettingImplCopyWithImpl<$Res>
? _value.locale
: locale // ignore: cast_nullable_to_non_nullable
as String?,
dashboardWidgets: null == dashboardWidgets
? _value._dashboardWidgets
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
as List<DashboardWidget>,
onlyProxy: null == onlyProxy
? _value.onlyProxy
: onlyProxy // ignore: cast_nullable_to_non_nullable
@@ -278,6 +295,8 @@ class __$$AppSettingImplCopyWithImpl<$Res>
class _$AppSettingImpl implements _AppSetting {
const _$AppSettingImpl(
{this.locale,
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
final List<DashboardWidget> dashboardWidgets = defaultDashboardWidgets,
this.onlyProxy = false,
this.autoLaunch = false,
this.silentLaunch = false,
@@ -290,13 +309,24 @@ class _$AppSettingImpl implements _AppSetting {
this.showLabel = false,
this.disclaimerAccepted = false,
this.minimizeOnExit = true,
this.hidden = false});
this.hidden = false})
: _dashboardWidgets = dashboardWidgets;
factory _$AppSettingImpl.fromJson(Map<String, dynamic> json) =>
_$$AppSettingImplFromJson(json);
@override
final String? locale;
final List<DashboardWidget> _dashboardWidgets;
@override
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> get dashboardWidgets {
if (_dashboardWidgets is EqualUnmodifiableListView)
return _dashboardWidgets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_dashboardWidgets);
}
@override
@JsonKey()
final bool onlyProxy;
@@ -339,7 +369,7 @@ class _$AppSettingImpl implements _AppSetting {
@override
String toString() {
return 'AppSetting(locale: $locale, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
return 'AppSetting(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
}
@override
@@ -348,6 +378,8 @@ class _$AppSettingImpl implements _AppSetting {
(other.runtimeType == runtimeType &&
other is _$AppSettingImpl &&
(identical(other.locale, locale) || other.locale == locale) &&
const DeepCollectionEquality()
.equals(other._dashboardWidgets, _dashboardWidgets) &&
(identical(other.onlyProxy, onlyProxy) ||
other.onlyProxy == onlyProxy) &&
(identical(other.autoLaunch, autoLaunch) ||
@@ -378,6 +410,7 @@ class _$AppSettingImpl implements _AppSetting {
int get hashCode => Object.hash(
runtimeType,
locale,
const DeepCollectionEquality().hash(_dashboardWidgets),
onlyProxy,
autoLaunch,
silentLaunch,
@@ -411,6 +444,8 @@ class _$AppSettingImpl implements _AppSetting {
abstract class _AppSetting implements AppSetting {
const factory _AppSetting(
{final String? locale,
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
final List<DashboardWidget> dashboardWidgets,
final bool onlyProxy,
final bool autoLaunch,
final bool silentLaunch,
@@ -431,6 +466,9 @@ abstract class _AppSetting implements AppSetting {
@override
String? get locale;
@override
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> get dashboardWidgets;
@override
bool get onlyProxy;
@override
bool get autoLaunch;

View File

@@ -54,6 +54,9 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
_$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> json) =>
_$AppSettingImpl(
locale: json['locale'] as String?,
dashboardWidgets: json['dashboardWidgets'] == null
? defaultDashboardWidgets
: dashboardWidgetsRealFormJson(json['dashboardWidgets'] as List?),
onlyProxy: json['onlyProxy'] as bool? ?? false,
autoLaunch: json['autoLaunch'] as bool? ?? false,
silentLaunch: json['silentLaunch'] as bool? ?? false,
@@ -72,6 +75,9 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
<String, dynamic>{
'locale': instance.locale,
'dashboardWidgets': instance.dashboardWidgets
.map((e) => _$DashboardWidgetEnumMap[e]!)
.toList(),
'onlyProxy': instance.onlyProxy,
'autoLaunch': instance.autoLaunch,
'silentLaunch': instance.silentLaunch,
@@ -87,6 +93,17 @@ Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
'hidden': instance.hidden,
};
const _$DashboardWidgetEnumMap = {
DashboardWidget.networkSpeed: 'networkSpeed',
DashboardWidget.outboundMode: 'outboundMode',
DashboardWidget.trafficUsage: 'trafficUsage',
DashboardWidget.networkDetection: 'networkDetection',
DashboardWidget.tunButton: 'tunButton',
DashboardWidget.systemProxyButton: 'systemProxyButton',
DashboardWidget.intranetIp: 'intranetIp',
DashboardWidget.memoryInfo: 'memoryInfo',
};
_$AccessControlImpl _$$AccessControlImplFromJson(Map<String, dynamic> json) =>
_$AccessControlImpl(
mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ??

View File

@@ -329,4 +329,6 @@ const _$ActionMethodEnumMap = {
ActionMethod.stopLog: 'stopLog',
ActionMethod.startListener: 'startListener',
ActionMethod.stopListener: 'stopListener',
ActionMethod.getCountryCode: 'getCountryCode',
ActionMethod.getMemory: 'getMemory',
};

View File

@@ -3209,137 +3209,6 @@ abstract class _ProxiesActionsState implements ProxiesActionsState {
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$AutoLaunchState {
bool get isAutoLaunch => throw _privateConstructorUsedError;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AutoLaunchStateCopyWith<AutoLaunchState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AutoLaunchStateCopyWith<$Res> {
factory $AutoLaunchStateCopyWith(
AutoLaunchState value, $Res Function(AutoLaunchState) then) =
_$AutoLaunchStateCopyWithImpl<$Res, AutoLaunchState>;
@useResult
$Res call({bool isAutoLaunch});
}
/// @nodoc
class _$AutoLaunchStateCopyWithImpl<$Res, $Val extends AutoLaunchState>
implements $AutoLaunchStateCopyWith<$Res> {
_$AutoLaunchStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isAutoLaunch = null,
}) {
return _then(_value.copyWith(
isAutoLaunch: null == isAutoLaunch
? _value.isAutoLaunch
: isAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$AutoLaunchStateImplCopyWith<$Res>
implements $AutoLaunchStateCopyWith<$Res> {
factory _$$AutoLaunchStateImplCopyWith(_$AutoLaunchStateImpl value,
$Res Function(_$AutoLaunchStateImpl) then) =
__$$AutoLaunchStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isAutoLaunch});
}
/// @nodoc
class __$$AutoLaunchStateImplCopyWithImpl<$Res>
extends _$AutoLaunchStateCopyWithImpl<$Res, _$AutoLaunchStateImpl>
implements _$$AutoLaunchStateImplCopyWith<$Res> {
__$$AutoLaunchStateImplCopyWithImpl(
_$AutoLaunchStateImpl _value, $Res Function(_$AutoLaunchStateImpl) _then)
: super(_value, _then);
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isAutoLaunch = null,
}) {
return _then(_$AutoLaunchStateImpl(
isAutoLaunch: null == isAutoLaunch
? _value.isAutoLaunch
: isAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$AutoLaunchStateImpl implements _AutoLaunchState {
const _$AutoLaunchStateImpl({required this.isAutoLaunch});
@override
final bool isAutoLaunch;
@override
String toString() {
return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AutoLaunchStateImpl &&
(identical(other.isAutoLaunch, isAutoLaunch) ||
other.isAutoLaunch == isAutoLaunch));
}
@override
int get hashCode => Object.hash(runtimeType, isAutoLaunch);
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AutoLaunchStateImplCopyWith<_$AutoLaunchStateImpl> get copyWith =>
__$$AutoLaunchStateImplCopyWithImpl<_$AutoLaunchStateImpl>(
this, _$identity);
}
abstract class _AutoLaunchState implements AutoLaunchState {
const factory _AutoLaunchState({required final bool isAutoLaunch}) =
_$AutoLaunchStateImpl;
@override
bool get isAutoLaunch;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AutoLaunchStateImplCopyWith<_$AutoLaunchStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProxyState {
bool get isStart => throw _privateConstructorUsedError;
@@ -4235,6 +4104,167 @@ abstract class _ClashConfigState implements ClashConfigState {
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$DashboardState {
List<DashboardWidget> get dashboardWidgets =>
throw _privateConstructorUsedError;
double get viewWidth => throw _privateConstructorUsedError;
/// Create a copy of DashboardState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$DashboardStateCopyWith<DashboardState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DashboardStateCopyWith<$Res> {
factory $DashboardStateCopyWith(
DashboardState value, $Res Function(DashboardState) then) =
_$DashboardStateCopyWithImpl<$Res, DashboardState>;
@useResult
$Res call({List<DashboardWidget> dashboardWidgets, double viewWidth});
}
/// @nodoc
class _$DashboardStateCopyWithImpl<$Res, $Val extends DashboardState>
implements $DashboardStateCopyWith<$Res> {
_$DashboardStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of DashboardState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? dashboardWidgets = null,
Object? viewWidth = null,
}) {
return _then(_value.copyWith(
dashboardWidgets: null == dashboardWidgets
? _value.dashboardWidgets
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
as List<DashboardWidget>,
viewWidth: null == viewWidth
? _value.viewWidth
: viewWidth // ignore: cast_nullable_to_non_nullable
as double,
) as $Val);
}
}
/// @nodoc
abstract class _$$DashboardStateImplCopyWith<$Res>
implements $DashboardStateCopyWith<$Res> {
factory _$$DashboardStateImplCopyWith(_$DashboardStateImpl value,
$Res Function(_$DashboardStateImpl) then) =
__$$DashboardStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<DashboardWidget> dashboardWidgets, double viewWidth});
}
/// @nodoc
class __$$DashboardStateImplCopyWithImpl<$Res>
extends _$DashboardStateCopyWithImpl<$Res, _$DashboardStateImpl>
implements _$$DashboardStateImplCopyWith<$Res> {
__$$DashboardStateImplCopyWithImpl(
_$DashboardStateImpl _value, $Res Function(_$DashboardStateImpl) _then)
: super(_value, _then);
/// Create a copy of DashboardState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? dashboardWidgets = null,
Object? viewWidth = null,
}) {
return _then(_$DashboardStateImpl(
dashboardWidgets: null == dashboardWidgets
? _value._dashboardWidgets
: dashboardWidgets // ignore: cast_nullable_to_non_nullable
as List<DashboardWidget>,
viewWidth: null == viewWidth
? _value.viewWidth
: viewWidth // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// @nodoc
class _$DashboardStateImpl implements _DashboardState {
const _$DashboardStateImpl(
{required final List<DashboardWidget> dashboardWidgets,
required this.viewWidth})
: _dashboardWidgets = dashboardWidgets;
final List<DashboardWidget> _dashboardWidgets;
@override
List<DashboardWidget> get dashboardWidgets {
if (_dashboardWidgets is EqualUnmodifiableListView)
return _dashboardWidgets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_dashboardWidgets);
}
@override
final double viewWidth;
@override
String toString() {
return 'DashboardState(dashboardWidgets: $dashboardWidgets, viewWidth: $viewWidth)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DashboardStateImpl &&
const DeepCollectionEquality()
.equals(other._dashboardWidgets, _dashboardWidgets) &&
(identical(other.viewWidth, viewWidth) ||
other.viewWidth == viewWidth));
}
@override
int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_dashboardWidgets), viewWidth);
/// Create a copy of DashboardState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$DashboardStateImplCopyWith<_$DashboardStateImpl> get copyWith =>
__$$DashboardStateImplCopyWithImpl<_$DashboardStateImpl>(
this, _$identity);
}
abstract class _DashboardState implements DashboardState {
const factory _DashboardState(
{required final List<DashboardWidget> dashboardWidgets,
required final double viewWidth}) = _$DashboardStateImpl;
@override
List<DashboardWidget> get dashboardWidgets;
@override
double get viewWidth;
/// Create a copy of DashboardState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$DashboardStateImplCopyWith<_$DashboardStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$VPNState {
AccessControl? get accessControl => throw _privateConstructorUsedError;

View File

@@ -0,0 +1,312 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of '../widget.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$ActivateState {
bool get active => throw _privateConstructorUsedError;
/// Create a copy of ActivateState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ActivateStateCopyWith<ActivateState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ActivateStateCopyWith<$Res> {
factory $ActivateStateCopyWith(
ActivateState value, $Res Function(ActivateState) then) =
_$ActivateStateCopyWithImpl<$Res, ActivateState>;
@useResult
$Res call({bool active});
}
/// @nodoc
class _$ActivateStateCopyWithImpl<$Res, $Val extends ActivateState>
implements $ActivateStateCopyWith<$Res> {
_$ActivateStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ActivateState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? active = null,
}) {
return _then(_value.copyWith(
active: null == active
? _value.active
: active // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$ActivateStateImplCopyWith<$Res>
implements $ActivateStateCopyWith<$Res> {
factory _$$ActivateStateImplCopyWith(
_$ActivateStateImpl value, $Res Function(_$ActivateStateImpl) then) =
__$$ActivateStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool active});
}
/// @nodoc
class __$$ActivateStateImplCopyWithImpl<$Res>
extends _$ActivateStateCopyWithImpl<$Res, _$ActivateStateImpl>
implements _$$ActivateStateImplCopyWith<$Res> {
__$$ActivateStateImplCopyWithImpl(
_$ActivateStateImpl _value, $Res Function(_$ActivateStateImpl) _then)
: super(_value, _then);
/// Create a copy of ActivateState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? active = null,
}) {
return _then(_$ActivateStateImpl(
active: null == active
? _value.active
: active // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$ActivateStateImpl implements _ActivateState {
const _$ActivateStateImpl({required this.active});
@override
final bool active;
@override
String toString() {
return 'ActivateState(active: $active)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ActivateStateImpl &&
(identical(other.active, active) || other.active == active));
}
@override
int get hashCode => Object.hash(runtimeType, active);
/// Create a copy of ActivateState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ActivateStateImplCopyWith<_$ActivateStateImpl> get copyWith =>
__$$ActivateStateImplCopyWithImpl<_$ActivateStateImpl>(this, _$identity);
}
abstract class _ActivateState implements ActivateState {
const factory _ActivateState({required final bool active}) =
_$ActivateStateImpl;
@override
bool get active;
/// Create a copy of ActivateState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ActivateStateImplCopyWith<_$ActivateStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$CommonMessage {
String get id => throw _privateConstructorUsedError;
String get text => throw _privateConstructorUsedError;
Duration get duration => throw _privateConstructorUsedError;
/// Create a copy of CommonMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CommonMessageCopyWith<CommonMessage> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CommonMessageCopyWith<$Res> {
factory $CommonMessageCopyWith(
CommonMessage value, $Res Function(CommonMessage) then) =
_$CommonMessageCopyWithImpl<$Res, CommonMessage>;
@useResult
$Res call({String id, String text, Duration duration});
}
/// @nodoc
class _$CommonMessageCopyWithImpl<$Res, $Val extends CommonMessage>
implements $CommonMessageCopyWith<$Res> {
_$CommonMessageCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CommonMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? text = null,
Object? duration = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as Duration,
) as $Val);
}
}
/// @nodoc
abstract class _$$CommonMessageImplCopyWith<$Res>
implements $CommonMessageCopyWith<$Res> {
factory _$$CommonMessageImplCopyWith(
_$CommonMessageImpl value, $Res Function(_$CommonMessageImpl) then) =
__$$CommonMessageImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String text, Duration duration});
}
/// @nodoc
class __$$CommonMessageImplCopyWithImpl<$Res>
extends _$CommonMessageCopyWithImpl<$Res, _$CommonMessageImpl>
implements _$$CommonMessageImplCopyWith<$Res> {
__$$CommonMessageImplCopyWithImpl(
_$CommonMessageImpl _value, $Res Function(_$CommonMessageImpl) _then)
: super(_value, _then);
/// Create a copy of CommonMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? text = null,
Object? duration = null,
}) {
return _then(_$CommonMessageImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as Duration,
));
}
}
/// @nodoc
class _$CommonMessageImpl implements _CommonMessage {
const _$CommonMessageImpl(
{required this.id,
required this.text,
this.duration = const Duration(seconds: 3)});
@override
final String id;
@override
final String text;
@override
@JsonKey()
final Duration duration;
@override
String toString() {
return 'CommonMessage(id: $id, text: $text, duration: $duration)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CommonMessageImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.text, text) || other.text == text) &&
(identical(other.duration, duration) ||
other.duration == duration));
}
@override
int get hashCode => Object.hash(runtimeType, id, text, duration);
/// Create a copy of CommonMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith =>
__$$CommonMessageImplCopyWithImpl<_$CommonMessageImpl>(this, _$identity);
}
abstract class _CommonMessage implements CommonMessage {
const factory _CommonMessage(
{required final String id,
required final String text,
final Duration duration}) = _$CommonMessageImpl;
@override
String get id;
@override
String get text;
@override
Duration get duration;
/// Create a copy of CommonMessage
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -5,3 +5,4 @@ export 'config.dart';
export 'core.dart';
export 'profile.dart';
export 'selector.dart';
export 'widget.dart';

View File

@@ -195,13 +195,6 @@ class ProxiesActionsState with _$ProxiesActionsState {
}) = _ProxiesActionsState;
}
@freezed
class AutoLaunchState with _$AutoLaunchState {
const factory AutoLaunchState({
required bool isAutoLaunch,
}) = _AutoLaunchState;
}
@freezed
class ProxyState with _$ProxyState {
const factory ProxyState({
@@ -244,6 +237,14 @@ class ClashConfigState with _$ClashConfigState {
}) = _ClashConfigState;
}
@freezed
class DashboardState with _$DashboardState {
const factory DashboardState({
required List<DashboardWidget> dashboardWidgets,
required double viewWidth,
}) = _DashboardState;
}
@freezed
class VPNState with _$VPNState {
const factory VPNState({

19
lib/models/widget.dart Normal file
View File

@@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/widget.freezed.dart';
@freezed
class ActivateState with _$ActivateState {
const factory ActivateState({
required bool active,
}) = _ActivateState;
}
@freezed
class CommonMessage with _$CommonMessage {
const factory CommonMessage({
required String id,
required String text,
@Default(Duration(seconds: 3)) Duration duration,
}) = _CommonMessage;
}

View File

@@ -13,104 +13,6 @@ typedef OnSelected = void Function(int index);
class HomePage extends StatelessWidget {
const HomePage({super.key});
_getNavigationBar({
required BuildContext context,
required ViewMode viewMode,
required List<NavigationItem> navigationItems,
required int currentIndex,
}) {
if (viewMode == ViewMode.mobile) {
return NavigationBar(
destinations: navigationItems
.map(
(e) => NavigationDestination(
icon: e.icon,
label: Intl.message(e.label),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
selectedIndex: currentIndex,
);
}
return LayoutBuilder(
builder: (_, container) {
return Material(
color: context.colorScheme.surfaceContainer,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
height: container.maxHeight,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: IntrinsicHeight(
child: Selector<Config, bool>(
selector: (_, config) => config.appSetting.showLabel,
builder: (_, showLabel, __) {
return NavigationRail(
backgroundColor:
context.colorScheme.surfaceContainer,
selectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
unselectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
selectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
unselectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected:
globalState.appController.toPage,
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
);
},
),
),
),
),
const SizedBox(
height: 16,
),
IconButton(
onPressed: () {
final config = globalState.appController.config;
final appSetting = config.appSetting;
config.appSetting = appSetting.copyWith(
showLabel: !appSetting.showLabel,
);
},
icon: const Icon(Icons.menu),
)
],
),
),
);
},
);
}
_updatePageController(List<NavigationItem> navigationItems) {
final currentLabel = globalState.appController.appState.currentLabel;
final index = navigationItems.lastIndexWhere(
@@ -177,8 +79,7 @@ class HomePage extends StatelessWidget {
(element) => element.label == currentLabel,
);
final currentIndex = index == -1 ? 0 : index;
final navigationBar = _getNavigationBar(
context: context,
final navigationBar = CommonNavigationBar(
viewMode: viewMode,
navigationItems: navigationItems,
currentIndex: currentIndex,
@@ -202,3 +103,121 @@ class HomePage extends StatelessWidget {
);
}
}
class CommonNavigationBar extends StatelessWidget {
final ViewMode viewMode;
final List<NavigationItem> navigationItems;
final int currentIndex;
const CommonNavigationBar({
super.key,
required this.viewMode,
required this.navigationItems,
required this.currentIndex,
});
_updateSafeMessageOffset(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final size = context.size;
if (viewMode == ViewMode.mobile) {
globalState.safeMessageOffsetNotifier.value = Offset(
0,
-(size?.height ?? 0),
);
} else {
globalState.safeMessageOffsetNotifier.value = Offset(
size?.width ?? 0,
0,
);
}
});
}
@override
Widget build(BuildContext context) {
_updateSafeMessageOffset(context);
if (viewMode == ViewMode.mobile) {
return NavigationBar(
destinations: navigationItems
.map(
(e) => NavigationDestination(
icon: e.icon,
label: Intl.message(e.label),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
selectedIndex: currentIndex,
);
}
return Material(
color: context.colorScheme.surfaceContainer,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 16,
),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: IntrinsicHeight(
child: Selector<Config, bool>(
selector: (_, config) => config.appSetting.showLabel,
builder: (_, showLabel, __) {
return NavigationRail(
backgroundColor: context.colorScheme.surfaceContainer,
selectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
unselectedIconTheme: IconThemeData(
color: context.colorScheme.onSurfaceVariant,
),
selectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
unselectedLabelTextStyle:
context.textTheme.labelLarge!.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
);
},
),
),
),
),
const SizedBox(
height: 16,
),
IconButton(
onPressed: () {
final config = globalState.appController.config;
final appSetting = config.appSetting;
config.appSetting = appSetting.copyWith(
showLabel: !appSetting.showLabel,
);
},
icon: const Icon(Icons.menu),
)
],
),
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/activate_box.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
@@ -113,14 +114,16 @@ class _ScanPageState extends State<ScanPage> with WidgetsBindingObserver {
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: AbsorbPointer(
absorbing: state.torchState == TorchState.unavailable,
child: ActivateBox(
active: state.torchState != TorchState.unavailable,
child: IconButton(
color: Colors.white,
icon: icon,
style: ButtonStyle(
foregroundColor: const WidgetStatePropertyAll(Colors.white),
backgroundColor: WidgetStatePropertyAll(backgroundColor),
foregroundColor:
const WidgetStatePropertyAll(Colors.white),
backgroundColor:
WidgetStatePropertyAll(backgroundColor),
),
onPressed: () => controller.toggleTorch(),
),
@@ -155,8 +158,8 @@ class _ScanPageState extends State<ScanPage> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
super.dispose();
}
}

View File

@@ -49,7 +49,7 @@ class App {
return Isolate.run<List<Package>>(() {
final List<dynamic> packagesRaw =
packagesString != null ? json.decode(packagesString) : [];
return packagesRaw.map((e) => Package.fromJson(e)).toList();
return packagesRaw.map((e) => Package.fromJson(e)).toSet().toList();
});
}

View File

@@ -24,6 +24,7 @@ class GlobalState {
PageController? pageController;
late Measure measure;
DateTime? startTime;
final safeMessageOffsetNotifier = ValueNotifier(Offset.zero);
final navigatorKey = GlobalKey<NavigatorState>();
late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
@@ -301,55 +302,33 @@ class GlobalState {
}
}
showSnackBar(
BuildContext context, {
required String message,
SnackBarAction? action,
}) {
final width = context.viewWidth;
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>(
FutureOr<T> Function() futureFunction, {
String? title,
bool silence = true,
}) async {
try {
final res = await futureFunction();
return res;
} catch (e) {
showMessage(
title: title ?? appLocalizations.tip,
message: TextSpan(
text: e.toString(),
),
);
if (silence) {
showNotifier(e.toString());
} else {
showMessage(
title: title ?? appLocalizations.tip,
message: TextSpan(
text: e.toString(),
),
);
}
return null;
}
}
showNotifier(String text) {
navigatorKey.currentContext?.showNotifier(text);
}
openUrl(String url) {
showMessage(
message: TextSpan(text: url),

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class ActivateBox extends StatelessWidget {
final Widget child;
final bool active;
const ActivateBox({
super.key,
required this.child,
this.active = false,
});
@override
Widget build(BuildContext context) {
return IgnorePointer(
ignoring: !active,
child: child,
);
}
}

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