Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef97ef40a1 | ||
|
|
9cb75f4814 | ||
|
|
375c4e0884 |
5
.github/release_template.md
vendored
5
.github/release_template.md
vendored
@@ -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>
|
||||
|
||||
67
.github/workflows/build.yaml
vendored
67
.github/workflows/build.yaml
vendored
@@ -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/
|
||||
|
||||
|
||||
66
.github/workflows/change.yaml
vendored
66
.github/workflows/change.yaml
vendored
@@ -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
2
.gitignore
vendored
@@ -5,9 +5,11 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,3 +1,45 @@
|
||||
## 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
|
||||
|
||||
29
README.md
29
README.md
@@ -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
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
[](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
|
||||
|
||||
@@ -72,6 +72,10 @@
|
||||
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 +92,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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,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))
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
22
core/hub.go
22
core/hub.go
@@ -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{
|
||||
|
||||
17
core/lib.go
17
core/lib.go
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(seconds: 5), () 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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -45,6 +45,10 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<String> getTotalTraffic(bool value);
|
||||
|
||||
FutureOr<String> getCountryCode(String ip);
|
||||
|
||||
FutureOr<String> getMemory();
|
||||
|
||||
resetTraffic();
|
||||
|
||||
startLog();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -343,8 +343,8 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_obscureController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/fragments/dashboard/widgets/intranet_ip.dart
Normal file
64
lib/fragments/dashboard/widgets/intranet_ip.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
111
lib/fragments/dashboard/widgets/memory_info.dart
Normal file
111
lib/fragments/dashboard/widgets/memory_info.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
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: () {},
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
250
lib/fragments/dashboard/widgets/network_detection.dart
Normal file
250
lib/fragments/dashboard/widgets/network_detection.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
124
lib/fragments/dashboard/widgets/network_speed.dart
Normal file
124
lib/fragments/dashboard/widgets/network_speed.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/fragments/dashboard/widgets/outbound_mode.dart
Normal file
73
lib/fragments/dashboard/widgets/outbound_mode.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
156
lib/fragments/dashboard/widgets/quick_options.dart
Normal file
156
lib/fragments/dashboard/widgets/quick_options.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
220
lib/fragments/dashboard/widgets/traffic_usage.dart
Normal file
220
lib/fragments/dashboard/widgets/traffic_usage.dart
Normal 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,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/fragments/dashboard/widgets/widgets.dart
Normal file
7
lib/fragments/dashboard/widgets/widgets.dart
Normal 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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -95,10 +95,10 @@ class _RequestsFragmentState extends State<RequestsFragment> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
timer?.cancel();
|
||||
_scrollController.dispose();
|
||||
timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -332,5 +332,6 @@
|
||||
"routeAddress": "路由地址",
|
||||
"routeAddressDesc": "配置监听路由地址",
|
||||
"pleaseInputAdminPassword": "请输入管理员密码",
|
||||
"copyEnvVar": "复制环境变量"
|
||||
"copyEnvVar": "复制环境变量",
|
||||
"memoryInfo": "内存信息"
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
43
lib/manager/connectivity_manager.dart
Normal file
43
lib/manager/connectivity_manager.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
326
lib/manager/message_manager.dart
Normal file
326
lib/manager/message_manager.dart
Normal file
@@ -0,0 +1,326 @@
|
||||
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.all(16),
|
||||
child: Text(
|
||||
message.text,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSecondaryFixedVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
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,
|
||||
spacing: 12,
|
||||
children: [
|
||||
for (final message in messages)
|
||||
_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']) ??
|
||||
|
||||
@@ -329,4 +329,6 @@ const _$ActionMethodEnumMap = {
|
||||
ActionMethod.stopLog: 'stopLog',
|
||||
ActionMethod.startListener: 'startListener',
|
||||
ActionMethod.stopListener: 'stopListener',
|
||||
ActionMethod.getCountryCode: 'getCountryCode',
|
||||
ActionMethod.getMemory: 'getMemory',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
312
lib/models/generated/widget.freezed.dart
Normal file
312
lib/models/generated/widget.freezed.dart
Normal 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;
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export 'config.dart';
|
||||
export 'core.dart';
|
||||
export 'profile.dart';
|
||||
export 'selector.dart';
|
||||
export 'widget.dart';
|
||||
|
||||
@@ -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
19
lib/models/widget.dart
Normal 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;
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,37 +302,6 @@ 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,
|
||||
@@ -340,16 +310,15 @@ class GlobalState {
|
||||
final res = await futureFunction();
|
||||
return res;
|
||||
} catch (e) {
|
||||
showMessage(
|
||||
title: title ?? appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
showNotifier(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
showNotifier(String text) {
|
||||
navigatorKey.currentContext?.showNotifier(text);
|
||||
}
|
||||
|
||||
openUrl(String url) {
|
||||
showMessage(
|
||||
message: TextSpan(text: url),
|
||||
|
||||
20
lib/widgets/activate_box.dart
Normal file
20
lib/widgets/activate_box.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
148
lib/widgets/bar_chart.dart
Normal file
148
lib/widgets/bar_chart.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class BarChartData {
|
||||
final double value;
|
||||
final String label;
|
||||
|
||||
const BarChartData({
|
||||
required this.value,
|
||||
required this.label,
|
||||
});
|
||||
}
|
||||
|
||||
class BarChart extends StatefulWidget {
|
||||
final List<BarChartData> data;
|
||||
final Duration duration;
|
||||
|
||||
const BarChart({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.duration = commonDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BarChart> createState() => _BarChartState();
|
||||
}
|
||||
|
||||
class _BarChartState extends State<BarChart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
late List<BarChartData> _oldData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_oldData = widget.data;
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
)..forward(from: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(BarChart oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.data != widget.data) {
|
||||
_oldData = oldWidget.data;
|
||||
_animationController.forward(from: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (_, container) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
painter: BarChartPainter(
|
||||
_oldData,
|
||||
widget.data,
|
||||
_animationController.value,
|
||||
),
|
||||
size: Size(container.maxWidth, container.maxHeight),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BarChartPainter extends CustomPainter {
|
||||
final List<BarChartData> oldData;
|
||||
final List<BarChartData> newData;
|
||||
final double progress;
|
||||
|
||||
BarChartPainter(this.oldData, this.newData, this.progress);
|
||||
|
||||
Map<String, Rect> getRectMap(List<BarChartData> dataList, Size size) {
|
||||
final spacing = size.width * 0.05;
|
||||
final maxBarWidth = 30;
|
||||
final barWidth =
|
||||
(size.width - spacing * (dataList.length - 1)) / dataList.length;
|
||||
final maxValue =
|
||||
dataList.fold(0.0, (max, item) => max > item.value ? max : item.value);
|
||||
final rects = <String, Rect>{};
|
||||
for (int i = 0; i < dataList.length; i++) {
|
||||
final data = dataList[i];
|
||||
double barHeight = (data.value / maxValue) * size.height;
|
||||
|
||||
final adjustLeft =
|
||||
barWidth > maxBarWidth ? (barWidth - maxBarWidth) / 2 : 0;
|
||||
double left = i * (barWidth + spacing) + adjustLeft;
|
||||
double top = size.height - barHeight;
|
||||
rects[data.label] = Rect.fromLTWH(
|
||||
left,
|
||||
top,
|
||||
min(barWidth, 30),
|
||||
barHeight,
|
||||
);
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final oldRectMap = getRectMap(oldData, size);
|
||||
final newRectMap = getRectMap(newData, size);
|
||||
|
||||
final paint = Paint()
|
||||
..color = Colors.blue
|
||||
..style = PaintingStyle.fill;
|
||||
final newRectEntries = newRectMap.entries.toList();
|
||||
for (int i = 0; i < newRectEntries.length; i++) {
|
||||
final newRectEntry = newRectEntries[i];
|
||||
final newRect = newRectEntry.value;
|
||||
final oldRect = oldRectMap[newRectEntry.key] ??
|
||||
newRect.translate(newRect.left * (progress - 1), 0);
|
||||
|
||||
final interpolatedRect = Rect.fromLTRB(
|
||||
lerpDouble(oldRect.left, newRect.left, progress)!,
|
||||
lerpDouble(oldRect.top, newRect.top, progress)!,
|
||||
lerpDouble(oldRect.right, newRect.right, progress)!,
|
||||
lerpDouble(oldRect.bottom, newRect.bottom, progress)!,
|
||||
);
|
||||
|
||||
canvas.drawRect(interpolatedRect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(BarChartPainter oldDelegate) {
|
||||
return oldDelegate.progress != progress ||
|
||||
oldDelegate.oldData != oldData ||
|
||||
oldDelegate.newData != newData;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ class _ScrollOverBuilderState extends State<ScrollOverBuilder> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
isOverNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -115,3 +115,22 @@ class ActiveBuilder extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeModeBuilder extends StatelessWidget {
|
||||
final StateWidgetBuilder<ThemeMode> builder;
|
||||
|
||||
const ThemeModeBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, ThemeMode>(
|
||||
selector: (_, config) => config.themeProps.themeMode,
|
||||
builder: (_, state, __) {
|
||||
return builder(state);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,19 @@ class Info {
|
||||
class InfoHeader extends StatelessWidget {
|
||||
final Info info;
|
||||
final List<Widget> actions;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const InfoHeader({
|
||||
super.key,
|
||||
required this.info,
|
||||
this.padding,
|
||||
List<Widget>? actions,
|
||||
}) : actions = actions ?? const [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
return Padding(
|
||||
padding: padding ?? baseInfoEdgeInsets,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -40,7 +42,7 @@ class InfoHeader extends StatelessWidget {
|
||||
if (info.iconData != null) ...[
|
||||
Icon(
|
||||
info.iconData,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
@@ -53,7 +55,9 @@ class InfoHeader extends StatelessWidget {
|
||||
info.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -80,12 +84,13 @@ class CommonCard extends StatelessWidget {
|
||||
const CommonCard({
|
||||
super.key,
|
||||
bool? isSelected,
|
||||
this.type = CommonCardType.plain,
|
||||
this.type = CommonCardType.filled,
|
||||
this.onPressed,
|
||||
this.info,
|
||||
this.selectWidget,
|
||||
this.backgroundColor,
|
||||
this.radius = 12,
|
||||
required this.child,
|
||||
this.info,
|
||||
}) : isSelected = isSelected ?? false;
|
||||
|
||||
final bool isSelected;
|
||||
@@ -95,15 +100,16 @@ class CommonCard extends StatelessWidget {
|
||||
final Info? info;
|
||||
final CommonCardType type;
|
||||
final double radius;
|
||||
final WidgetStateProperty<Color?>? backgroundColor;
|
||||
|
||||
BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) {
|
||||
if (type == CommonCardType.filled) {
|
||||
return BorderSide.none;
|
||||
}
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final colorScheme = context.colorScheme;
|
||||
// if (type == CommonCardType.filled) {
|
||||
// return BorderSide.none;
|
||||
// }
|
||||
final hoverColor = isSelected
|
||||
? colorScheme.primary.toLight()
|
||||
: colorScheme.primary.toLighter();
|
||||
? colorScheme.primary.toLight
|
||||
: colorScheme.primary.toLighter;
|
||||
if (states.contains(WidgetState.hovered) ||
|
||||
states.contains(WidgetState.focused) ||
|
||||
states.contains(WidgetState.pressed)) {
|
||||
@@ -112,19 +118,19 @@ class CommonCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return BorderSide(
|
||||
color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
|
||||
color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft,
|
||||
);
|
||||
}
|
||||
|
||||
Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final colorScheme = context.colorScheme;
|
||||
switch (type) {
|
||||
case CommonCardType.plain:
|
||||
if (isSelected) {
|
||||
return colorScheme.secondaryContainer;
|
||||
}
|
||||
if (states.isEmpty) {
|
||||
return colorScheme.secondaryContainer.toLittle();
|
||||
return colorScheme.surface;
|
||||
}
|
||||
return Theme.of(context)
|
||||
.outlinedButtonTheme
|
||||
@@ -135,7 +141,7 @@ class CommonCard extends StatelessWidget {
|
||||
if (isSelected) {
|
||||
return colorScheme.secondaryContainer;
|
||||
}
|
||||
return colorScheme.surfaceContainer;
|
||||
return colorScheme.surfaceContainerLow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +154,9 @@ class CommonCard extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InfoHeader(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
bottom: 0,
|
||||
),
|
||||
info: info!,
|
||||
),
|
||||
Flexible(
|
||||
@@ -157,6 +166,7 @@ class CommonCard extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (selectWidget != null && isSelected) {
|
||||
final List<Widget> children = [];
|
||||
children.add(childWidget);
|
||||
@@ -169,6 +179,7 @@ class CommonCard extends StatelessWidget {
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
style: ButtonStyle(
|
||||
@@ -178,9 +189,12 @@ class CommonCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) => getBackgroundColor(context, states),
|
||||
),
|
||||
iconColor: WidgetStatePropertyAll(context.colorScheme.primary),
|
||||
iconSize: WidgetStateProperty.all(20),
|
||||
backgroundColor: backgroundColor ??
|
||||
WidgetStateProperty.resolveWith(
|
||||
(states) => getBackgroundColor(context, states),
|
||||
),
|
||||
side: WidgetStateProperty.resolveWith(
|
||||
(states) => getBorderSide(context, states),
|
||||
),
|
||||
|
||||
175
lib/widgets/donut_chart.dart
Normal file
175
lib/widgets/donut_chart.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'dart:math';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class DonutChartData {
|
||||
final double _value;
|
||||
final Color color;
|
||||
|
||||
const DonutChartData({
|
||||
required double value,
|
||||
required this.color,
|
||||
}) : _value = value + 1;
|
||||
|
||||
double get value => _value;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DonutChartData{_value: $_value}';
|
||||
}
|
||||
}
|
||||
|
||||
class DonutChart extends StatefulWidget {
|
||||
final List<DonutChartData> data;
|
||||
final Duration duration;
|
||||
|
||||
const DonutChart({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.duration = commonDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DonutChart> createState() => _DonutChartState();
|
||||
}
|
||||
|
||||
class _DonutChartState extends State<DonutChart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late List<DonutChartData> _oldData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_oldData = widget.data;
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DonutChart oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.data != widget.data) {
|
||||
_oldData = oldWidget.data;
|
||||
_animationController.forward(from: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
painter: DonutChartPainter(
|
||||
_oldData,
|
||||
widget.data,
|
||||
_animationController.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DonutChartPainter extends CustomPainter {
|
||||
final List<DonutChartData> oldData;
|
||||
final List<DonutChartData> newData;
|
||||
final double progress;
|
||||
|
||||
DonutChartPainter(this.oldData, this.newData, this.progress);
|
||||
|
||||
double _logTransform(double value) {
|
||||
const base = 10.0;
|
||||
const minValue = 0.1;
|
||||
if (value < minValue) return 0;
|
||||
return log(value) / log(base) + 1;
|
||||
}
|
||||
|
||||
double _expTransform(double value) {
|
||||
const base = 10.0;
|
||||
if (value <= 0) return 0;
|
||||
return pow(base, value - 1).toDouble();
|
||||
}
|
||||
|
||||
List<DonutChartData> get interpolatedData {
|
||||
if (oldData.length != newData.length) return newData;
|
||||
|
||||
final interpolatedData = List.generate(newData.length, (index) {
|
||||
final oldValue = oldData[index].value;
|
||||
final newValue = newData[index].value;
|
||||
|
||||
final logOldValue = _logTransform(oldValue);
|
||||
final logNewValue = _logTransform(newValue);
|
||||
final interpolatedLogValue =
|
||||
logOldValue + (logNewValue - logOldValue) * progress;
|
||||
|
||||
final interpolatedValue = _expTransform(interpolatedLogValue);
|
||||
|
||||
return DonutChartData(
|
||||
value: interpolatedValue,
|
||||
color: newData[index].color,
|
||||
);
|
||||
});
|
||||
|
||||
return interpolatedData;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
const strokeWidth = 10.0;
|
||||
final radius = min(size.width / 2, size.height / 2) - strokeWidth / 2;
|
||||
|
||||
final gapAngle = 2 * asin(strokeWidth * 1 / (2 * radius)) * 1.2;
|
||||
|
||||
final data = interpolatedData;
|
||||
final total = data.fold<double>(
|
||||
0,
|
||||
(sum, item) => sum + item.value,
|
||||
);
|
||||
|
||||
if (total <= 0) return;
|
||||
|
||||
final availableAngle = 2 * pi - (data.length * gapAngle);
|
||||
double startAngle = -pi / 2 + gapAngle / 2;
|
||||
|
||||
for (final item in data) {
|
||||
final sweepAngle = availableAngle * (item.value / total);
|
||||
|
||||
if (sweepAngle <= 0) continue;
|
||||
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round
|
||||
..color = item.color;
|
||||
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
startAngle,
|
||||
sweepAngle,
|
||||
false,
|
||||
paint,
|
||||
);
|
||||
|
||||
startAngle += sweepAngle + gapAngle;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(DonutChartPainter oldDelegate) {
|
||||
return oldDelegate.progress != progress ||
|
||||
oldDelegate.oldData != oldData ||
|
||||
oldDelegate.newData != newData;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
typedef WrapBuilder = Widget Function(Widget child);
|
||||
|
||||
class Grid extends MultiChildRenderObjectWidget {
|
||||
final double mainAxisSpacing;
|
||||
|
||||
@@ -362,6 +364,18 @@ class GridItem extends ParentDataWidget<GridParentData> {
|
||||
|
||||
@override
|
||||
Type get debugTypicalAncestorWidgetClass => GridItem;
|
||||
|
||||
GridItem wrap({
|
||||
required WrapBuilder builder,
|
||||
}) {
|
||||
return GridItem(
|
||||
mainAxisCellCount: mainAxisCellCount,
|
||||
crossAxisCellCount: crossAxisCellCount,
|
||||
child: builder(
|
||||
child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Origin {
|
||||
@@ -372,11 +386,11 @@ class _Origin {
|
||||
}
|
||||
|
||||
_Origin _getOrigin(List<double> offsets, int crossAxisCount) {
|
||||
var length = offsets.length;
|
||||
var origin = const _Origin(0, double.infinity);
|
||||
final length = offsets.length;
|
||||
_Origin origin = const _Origin(0, double.infinity);
|
||||
for (int i = 0; i < length; i++) {
|
||||
final offset = offsets[i];
|
||||
if (offset.lessOrEqual(origin.mainAxisOffset)) {
|
||||
if (offset.moreOrEqual(origin.mainAxisOffset)) {
|
||||
continue;
|
||||
}
|
||||
int start = 0;
|
||||
@@ -386,7 +400,7 @@ _Origin _getOrigin(List<double> offsets, int crossAxisCount) {
|
||||
j < length &&
|
||||
length - j >= crossAxisCount - span;
|
||||
j++) {
|
||||
if (offset.lessOrEqual(offsets[j])) {
|
||||
if (offset.moreOrEqual(offsets[j])) {
|
||||
span++;
|
||||
if (span == crossAxisCount) {
|
||||
origin = _Origin(start, offset);
|
||||
@@ -399,19 +413,3 @@ _Origin _getOrigin(List<double> offsets, int crossAxisCount) {
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
|
||||
extension on double {
|
||||
lessOrEqual(double value) {
|
||||
return value < this || (value - this).abs() < precisionErrorTolerance + 1;
|
||||
}
|
||||
}
|
||||
|
||||
extension on Offset {
|
||||
double getCrossAxisOffset(Axis direction) {
|
||||
return direction == Axis.vertical ? dx : dy;
|
||||
}
|
||||
|
||||
double getMainAxisOffset(Axis direction) {
|
||||
return direction == Axis.vertical ? dy : dx;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user