Compare commits

..

19 Commits

Author SHA1 Message Date
chen08209
526ccdf3ad Fix the collapse issues
Add fontFamily options
2024-10-26 16:52:10 +08:00
chen08209
8282a9a474 Update core version
Update flutter version

Optimize ip check

Optimize url-test
2024-10-26 01:46:55 +08:00
chen08209
dfa6d31673 Update release message
Init auto gen changelog
2024-10-12 17:53:40 +08:00
chen08209
89bbbc6864 Fix windows tray issues
Fix urltest issues

Add auto changelog
2024-10-12 15:06:55 +08:00
chen08209
a3e1b38201 Fix windows admin auto launch issues
Add android vpn options

Support proxies icon configuration

Optimize android immersion display

Fix some issues
2024-10-12 15:06:55 +08:00
chen08209
4e3dc45f13 Optimize ip detection
Support android vpn ipv6 inbound switch

Support log export

Optimize more details
2024-10-12 15:06:55 +08:00
chen08209
13d31cf708 Fix android system dns issues
Optimize dns default option

Fix some issues
2024-10-12 15:06:54 +08:00
chen08209
62a7772b92 Update readme 2024-10-12 15:06:54 +08:00
chen08209
043648f998 Fix build error2 2024-09-17 23:06:32 +08:00
chen08209
3eb26e8061 Fix build error 2024-09-17 22:42:10 +08:00
chen08209
5d6bd6466f Support desktop hotkey
Support android ipv6 inbound

Support android system dns

fix some bugs
2024-09-17 21:42:13 +08:00
chen08209
4e766d9407 Fix delete profile error 2024-09-09 09:49:22 +08:00
chen08209
80f8aa22ee Fix submit error 2 2024-09-08 21:38:58 +08:00
chen08209
97714e8b25 Fix submit error 2024-09-08 21:22:02 +08:00
chen08209
50bf4170d9 Optimize DNS strategy
Fix the problem that the tray is not displayed in some cases

Optimize tray

Update core

Fix some error
2024-09-08 20:58:02 +08:00
chen08209
79efa67df3 Fix tun update issues 2024-09-02 16:42:47 +08:00
chen08209
ac397393a0 Add DNS override
Fixed some bugs
Optimize more detail
2024-09-02 16:09:51 +08:00
chen08209
b685165230 Add Hosts override 2024-09-02 16:09:27 +08:00
chen08209
402221aaa2 fix android tip error
fix windows auto launch error
2024-08-26 20:35:38 +08:00
239 changed files with 52138 additions and 37020 deletions

57
.github/release_template.md vendored Normal file
View File

@@ -0,0 +1,57 @@
<div align=center>
[![Release Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/vVERSION/total?style=flat-square&logo=github)](https://img.shields.io/github/downloads/chen08209/FlClash/vVERSION/)
</div>
**Download based on your OS:**
<div align=left>
<table>
<thead align=left>
<tr>
<th>OS</th>
<th>Download</th>
</tr>
</thead>
<tbody align=left>
<tr>
<td>Android</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-arm64-v8a.apk"><img src="https://img.shields.io/badge/APK-ARMv8-168039.svg?logo=android"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-armeabi-v7a.apk"><img src="https://img.shields.io/badge/APK-ARMv7-45bf55.svg?logo=android"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-x86_64.apk"><img src="https://img.shields.io/badge/APK-x64-96ed89.svg?logo=android"></a>
</td>
</tr>
<tr>
<td>Windows</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-windows-amd64-setup.exe"><img src="https://img.shields.io/badge/Setup-x64-2d7d9a.svg?logo=windows"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-windows-amd64.zip"><img src="https://img.shields.io/badge/Portable-x64-67b7d1.svg?logo=windows"></a>
</td>
</tr>
<tr>
<td>macOS (v10.15+)</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>
</td>
</tr>
<tr>
<td>Linux</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.AppImage"><img src="https://img.shields.io/badge/AppImage-x64-f84e29.svg?logo=linux"> </a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.deb"><img src="https://img.shields.io/badge/DebPackage-x64-FF9966.svg?logo=debian"> </a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.deb"><img src="https://img.shields.io/badge/RpmPackage-x64-F1B42F.svg?logo=redhat"> </a>
</td>
</tr>
</tbody>
</table>
</div>
<div dir="ltr">
**List of all changes:** [ChangeLog](https://github.com/chen08209/FlClash/blob/main/CHANGELOG.md)
</div>

View File

@@ -35,7 +35,6 @@ jobs:
install: mingw-w64-x86_64-gcc
update: true
- name: Set Mingw64 Env
if: startsWith(matrix.platform,'windows')
run: |
@@ -87,7 +86,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 3.22.x
flutter-version: '3.x'
channel: 'stable'
cache: true
@@ -102,15 +101,21 @@ jobs:
with:
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
path: ./dist
retention-days: 1
overwrite: true
upload-release:
if: ${{ !contains(github.ref, '+') }}
upload:
permissions: write-all
needs: [ build ]
runs-on: ubuntu-latest
services:
telegram-bot-api:
image: aiogram/telegram-bot-api:latest
env:
TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
ports:
- 8081:8081
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -124,32 +129,63 @@ jobs:
pattern: artifact-*
merge-multiple: true
- name: Pre Release
- name: Generate release.md
run: |
pip install gitchangelog pystache mustache markdown
pre=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
if [ -z "pre" ]; then
echo "init" > release.md
else
current="${{ github.ref_name }}"
echo -e "\n\n<details markdown=1><summary>All changes from $current to the latest commit:</summary>\n\n" >> release.md
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
echo -e "\n\n</details>" >> release.md
fi
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
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
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
fi
echo "" >> release.md
fi
currentTag=$tag
done
- name: Push to telegram
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
python -m pip install --upgrade pip
pip install requests
python release.py
- name: Patch release.md
run: |
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Release
if: ${{ !contains(github.ref, '+') }}
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
if: ${{ !contains(github.ref, '+') }}
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo
if: ${{ !contains(github.ref, '+') }}
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
@@ -161,4 +197,4 @@ 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/

45
.github/workflows/change.yaml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: change
on:
push:
branches:
- 'main'
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate
run: |
tags=$(git tag --sort=creatordate)
previous=""
if [ ! -f CHANGELOG.md ]; then
echo "" > CHANGELOG.md
else
previous=$(grep -oP '^## \K.*' CHANGELOG.md | tail -n 1)
fi
for tag in $tags; do
if [ -n "$previous" ]; then
echo "## $tag" >> CHANGELOG.md
git log --pretty=format:"* %s (%h)" "$previous..$tag" >> CHANGELOG.md
echo -e "\n" >> CHANGELOG.md
fi
previous=$tag
done
- name: Commit
run: |
if git diff --cached --quiet; then
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add CHANGELOG.md
git commit -m "Update Changelog"
git push
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitmodules vendored
View File

@@ -1,7 +1,7 @@
[submodule "core/Clash.Meta"]
path = core/Clash.Meta
url = git@github.com:chen08209/Clash.Meta.git
branch = FlClash
branch = FlClash-Alpha
[submodule "plugins/flutter_distributor"]
path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git

630
CHANGELOG.md Normal file
View File

@@ -0,0 +1,630 @@
## v0.8.63
- Fix windows admin auto launch issues
- Add android vpn options
- Support proxies icon configuration
- Optimize android immersion display
- Fix some issues
## v0.8.62
- Optimize ip detection
- Support android vpn ipv6 inbound switch
- Support log export
- Optimize more details
- Fix android system dns issues
- Optimize dns default option
- Fix some issues
- Update readme
- Update README.md 2
- Update README.md 2
- Update README.md
## v0.8.60
- Fix build error2
- Fix build error
- Support desktop hotkey
- Support android ipv6 inbound
- Support android system dns
- fix some bugs
## v0.8.59
- Fix delete profile error
## v0.8.58
- Fix submit error 2
- Fix submit error
- Optimize DNS strategy
- Fix the problem that the tray is not displayed in some cases
- Optimize tray
- Update core
- Fix some error
## v0.8.57
- Fix tun update issues
- Add DNS override
- Fixed some bugs
- Optimize more detail
- Add Hosts override
## v0.8.56
- fix android tip error
- fix windows auto launch error
## v0.8.55
- Fix windows tray issues
- Optimize windows logic
- Optimize app logic
- Support windows administrator auto launch
- Support android close vpn
## v0.8.53
- Change flutter version
- Support profiles sort
- Support windows country flags display
- Optimize proxies page and profiles page columns
## v0.8.52
- Update flutter version
- Update version
- Update timeout time
- Update access control page
- Fix bug
## v0.8.51
- Optimize provider page
- Optimize delay test
- Support local backup and recovery
- Fix android tile service issues
## v0.8.49
- Fix linux core build error
- Add proxy-only traffic statistics
- Update core
- Optimize more details
- Merge pull request #140 from txyyh/main
- 添加自建 F-Droid 仓库相关 workflow
- Rename readme fingerprint
- Rename workflow deploy repo name
- Add download guide to README
- Add push release files to fdroid-repo
## v0.8.48
- Optimize proxies page
- Fix ua issues
- Optimize more details
## v0.8.47
- Fix windows build error
## v0.8.46
- Update app icon
- Fix desktop backup error
- Optimize request ua
- Change android icon
- Optimize dashboard
## v0.8.44
- Remove request validate certificate
- Sync core
## v0.8.43
- Fix windows error
## v0.8.42
- Fix setup.dart error
- Fix android system proxy not effective
- Add macos arm64
## v0.8.41
- Optimize proxies page
- Support mouse drag scroll
- Adjust desktop ui
- Revert "Fix android vpn issues"
- This reverts commit 891977408e6938e2acd74e9b9adb959c48c79988.
## v0.8.40
- Fix android vpn issues
- Fix android vpn issues
- Rollback partial modification
## v0.8.39
- Fix the problem that ui can't be synchronized when android vpn is occupied by an external
- Override default socksPort,port
## v0.8.38
- Fix fab issues
## v0.8.37
- Update version
- Fix the problem that vpn cannot be started in some cases
- Fix the problem that geodata url does not take effect
## v0.8.36
- Update ua
- Fix change outbound mode without check ip issues
- Separate android ui and vpn
- Fix url validate issues 2
- Add android hidden from the recent task
- Add geoip file
- Support modify geoData URL
## v0.8.35
- Fix url validate issues
- Fix check ip performance problem
- Optimize resources page
## v0.8.34
- Add ua selector
- Support modify test url
- Optimize android proxy
- Fix the error that async proxy provider could not selected the proxy
## v0.8.33
- Fix android proxy error
- Fix submit error
- Add windows tun
- Optimize android proxy
- Optimize change profile
- Update application ua
- Optimize delay test
## v0.8.32
- Fix android repeated request notification issues
## v0.8.31
- Fix memory overflow issues
## v0.8.30
- Optimize proxies expansion panel 2
- Fix android scan qrcode error
## v0.8.29
- Optimize proxies expansion panel
- Fix text error
## v0.8.28
- Optimize proxy
- Optimize delayed sorting performance
- Add expansion panel proxies page
- Support to adjust the proxy card size
- Support to adjust proxies columns number
- Fix autoRun show issues
- Fix Android 10 issues
- Optimize ip show
## v0.8.26
- Add intranet IP display
- Add connections page
- Add search in connections, requests
- Add keyword search in connections, requests, logs
- Add basic viewing editing capabilities
- Optimize update profile
## v0.8.25
- Update version
- Fix the problem of excessive memory usage in traffic usage.
- Add lightBlue theme color
- Fix start unable to update profile issues
- Fix flashback caused by process
## v0.8.23
- Add build version
- Optimize quick start
- Update system default option
## v0.8.22
- Update build.yml
- Fix android vpn close issues
- Add requests page
- Fix checkUpdate dark mode style error
- Fix quickStart error open app
- Add memory proxies tab index
- Support hidden group
- Optimize logs
- Fix externalController hot load error
## v0.8.21
- Add tcp concurrent switch
- Add system proxy switch
- Add geodata loader switch
- Add external controller switch
- Add auto gc on trim memory
- Fix android notification error
## v0.8.20
- Fix ipv6 error
- Fix android udp direct error
- Add ipv6 switch
- Add access all selected button
- Remove android low version splash
## v0.8.19
- Update version
- Add allowBypass
- Fix Android only pick .text file issues
## v0.8.18
- Fix search issues
## v0.8.17
- Fix LoadBalance, Relay load error
- Fix build.yml4
- Fix build.yml3
- Fix build.yml2
- Fix build.yml
- Add search function at access control
- Fix the issues with the profile add button to cover the edit button
- Adapt LoadBalance and Relay
- Add arm
- Fix android notification icon error
## v0.8.16
- Add one-click update all profiles
- Add expire show
## v0.8.15
- Temp remove tun mode
- Remove macos in workflow
- Change go version
## v0.8.14
- Update Version
- Fix tun unable to open
## v0.8.13
- Optimize delay test2
- Optimize delay test
- Add check ip
- add check ip request
## v0.8.12
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
- Fix edit profile error
- Fix quickStart change proxy error
- Fix core version
## v0.8.10
- Fix core version
## v0.8.9
- Update file_picker
- Add resources page
- Optimize more detail
- Add access selected sorted
- Fix notification duplicate creation issue
- Fix AccessControl click issue
## v0.8.7
- Fix Workflow
- Fix Linux unable to open
- Update README.md 3
- Create LICENSE
- Update README.md 2
- Update README.md
- Optimize workFlow
## v0.8.6
- optimize checkUpdate
## v0.8.5
- Fix submit error
## v0.8.4
- add WebDAV
- add Auto check updates
- Optimize more details
- optimize delayTest
## v0.8.2
- upgrade flutter version
## v0.8.1
- Update kernel
- Add import profile via QR code image
## v0.8.0
- Add compatibility mode and adapt clash scheme.
## v0.7.14
- update Version
- Reconstruction application proxy logic
## v0.7.13
- Fix Tab destroy error
## v0.7.12
- Optimize repeat healthcheck
## v0.7.11
- Optimize Direct mode ui
## v0.7.10
- Optimize Healthcheck
- Remove proxies position animation, improve performance
- Add Telegram Link
- Update healthcheck policy
- New Check URLTest
- Fix the problem of invalid auto-selection
## v0.7.8
- New Async UpdateConfig
- add changeProfileDebounce
- Update Workflow
- Fix ChangeProfile block
- Fix Release Message Error
## v0.7.7
- Update Selector 2
## v0.7.6
- Update Version
- Fix Proxies Select Error
## v0.7.5
- Fix the problem that the proxy group is empty in global mode.
- Fix the problem that the proxy group is empty in global mode.
## v0.7.4
- Add ProxyProvider2
## v0.7.3
- Add ProxyProvider
- Update Version
- Update ProxyGroup Sort
- Fix Android quickStart VpnService some problems
## v0.7.1
- Update version
- Set Android notification low importance
- Fix the issue that VpnService can't be closed correctly in special cases
- Fix the problem that TileService is not destroyed correctly in some cases
- Adjust tab animation defaults
- Add Telegram in README_zh_CN.md
- Add Telegram
## v0.7.0
- update mobile_scanner
- Initial commit

View File

@@ -6,13 +6,9 @@
## FlClash
<p style="text-align: left;">
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
<a href="LICENSE">
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
</a>
</p>
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
@@ -42,10 +38,6 @@ on Mobile:
<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>
## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build
1. Update submodules
@@ -100,9 +92,6 @@ on Mobile:
```bash
dart .\setup.dart
```
## Star

View File

@@ -6,13 +6,10 @@
## FlClash
<p style="text-align: left;">
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
<img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
<a href="LICENSE">
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
</a>
</p>
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE)
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
基于ClashMeta的多平台代理客户端简单易用开源无广告。
@@ -42,11 +39,6 @@ on Mobile:
<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>
## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build
1. 更新 submodules

View File

@@ -34,22 +34,22 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android {
namespace "com.follow.clash"
compileSdkVersion 34
ndkVersion "25.1.8937393"
ndkVersion "27.1.12297006"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
signingConfigs {
if (isRelease){
if (isRelease) {
release {
storeFile defStoreFile
storePassword defStorePassword
@@ -74,10 +74,9 @@ android {
applicationIdSuffix '.debug'
}
release {
minifyEnabled true
if(isRelease){
if (isRelease) {
signingConfig signingConfigs.release
}else{
} else {
signingConfig signingConfigs.debug
}
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"

View File

@@ -10,25 +10,22 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name="${applicationName}"
android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu">
android:hardwareAccelerated="true"
android:label="FlClash">
<activity
android:name="com.follow.clash.MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -72,7 +69,17 @@
<activity
android:name=".TempActivity"
android:theme="@style/TransparentTheme" />
android:exported="true"
android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.START" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.STOP" />
</intent-filter>
</activity>
<service
android:name=".services.FlClashTileService"
@@ -119,12 +126,19 @@
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="specialUse" />
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />
</service>
<meta-data
android:name="flutterEmbedding"

View File

@@ -1,9 +1,10 @@
package com.follow.clash
import com.follow.clash.models.Props
import com.follow.clash.models.VpnOptions
interface BaseServiceInterface {
fun start(port: Int, props: Props?): Int?
fun start(options: VpnOptions): Int
fun stop()
fun startForeground(title: String, content: String)
}

View File

@@ -33,14 +33,13 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun getCurrentTitlePlugin(): TilePlugin? {
fun getCurrentTilePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
fun getCurrentVPNPlugin(): VpnPlugin? {
val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}
fun destroyServiceEngine() {

View File

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

View File

@@ -6,6 +6,15 @@ import android.os.Bundle
class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
"com.follow.clash.action.START" -> {
GlobalState.getCurrentTilePlugin()?.handleStart()
}
"com.follow.clash.action.STOP" -> {
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}
finishAndRemoveTask()
}
}

View File

@@ -1,28 +1,21 @@
package com.follow.clash.extensions
import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Build
import android.net.ConnectivityManager
import android.net.Network
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.models.CIDR
import com.follow.clash.models.Metadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
suspend fun Drawable.getBase64(): String {
@@ -41,6 +34,56 @@ fun Metadata.getProtocol(): Int? {
return null
}
private val CHANNEL = "FlClash"
fun String.toCIDR(): CIDR {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val ipAddress = parts[0]
val prefixLength = parts[1].toIntOrNull()
?: throw IllegalArgumentException("Invalid prefix length")
private val notificationId: Int = 1
val address = InetAddress.getByName(ipAddress)
val maxPrefix = if (address.address.size == 4) 32 else 128
if (prefixLength < 0 || prefixLength > maxPrefix) {
throw IllegalArgumentException("Invalid prefix length for IP version")
}
return CIDR(address, prefixLength)
}
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.asSocketAddressText(53) }
}
fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) {
is Inet6Address ->
"[${numericToTextFormat(this.address)}]:$port"
is Inet4Address ->
"${this.hostAddress}:$port"
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
}
}
private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39)
for (i in 0 until 8) {
sb.append(
Integer.toHexString(
src[i shl 1].toInt() shl 8 and 0xff00
or (src[(i shl 1) + 1].toInt() and 0xff)
)
)
if (i < 7) {
sb.append(":")
}
}
return sb.toString()
}

View File

@@ -1,7 +1,5 @@
package com.follow.clash.models
import java.util.Date
data class Package(
val packageName: String,
val label: String,

View File

@@ -1,5 +1,7 @@
package com.follow.clash.models
import java.net.InetAddress
enum class AccessControlMode {
acceptSelected,
rejectSelected,
@@ -11,9 +13,16 @@ data class AccessControl(
val rejectList: List<String>,
)
data class Props(
val enable: Boolean?,
data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions(
val enable: Boolean,
val port: Int,
val accessControl: AccessControl?,
val allowBypass: Boolean?,
val systemProxy: Boolean?,
)
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,
val ipv4Address: String,
val ipv6Address: String,
val dnsServerAddress: String,
)

View File

@@ -8,7 +8,6 @@ import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.VpnService
import android.os.Build
import android.widget.Toast
@@ -17,12 +16,9 @@ import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import com.follow.clash.GlobalState
import com.follow.clash.extensions.getBase64
import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Package
import com.follow.clash.models.Process
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -37,7 +33,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.net.InetSocketAddress
import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
@@ -52,11 +47,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private lateinit var scope: CoroutineScope
private var connectivity: ConnectivityManager? = null
private var vpnCallBack: (() -> Unit)? = null
private val iconMap = mutableMapOf<String, String?>()
private val packages = mutableListOf<Package>()
private val skipPrefixList = listOf(
@@ -114,7 +108,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
@@ -191,48 +184,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
"resolverProcess" -> {
val data = call.argument<String>("data")
val process =
if (data != null) Gson().fromJson(
data,
Process::class.java
) else null
val metadata = process?.metadata
val protocol = metadata?.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
if (connectivity == null) {
connectivity = context.getSystemService<ConnectivityManager>()
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages = context.packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
"tip" -> {
val message = call.argument<String>("message")
tip(message)
@@ -379,7 +330,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
private fun isChinaPackage(packageName: String): Boolean {
val packageManager = context.packageManager ?: return false
skipPrefixList.forEach {
@@ -447,10 +397,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return false
}
fun requestGc() {
channel.invokeMethod("gc", null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
binding.addActivityResultListener(::onActivityResult)
@@ -490,4 +436,4 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
return true
}
}
}

View File

@@ -1,6 +1,8 @@
package com.follow.clash.plugins
import android.content.Context
import android.net.ConnectivityManager
import androidx.core.content.getSystemService
import com.follow.clash.GlobalState
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall

View File

@@ -1,31 +1,47 @@
package com.follow.clash.plugins
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.content.getSystemService
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.models.Props
import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns
import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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 {
private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null
private var port: Int = 7890
private var props: Props? = null
private lateinit var options: VpnOptions
private lateinit var scope: CoroutineScope
private val connectivity by lazy {
context.getSystemService<ConnectivityManager>()
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
@@ -43,66 +59,169 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext
scope.launch {
registerNetworkCallback()
}
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
unRegisterNetworkCallback()
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"start" -> {
port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
when (props?.enable == true) {
true -> handleStartVpn()
false -> start()
}
result.success(true)
}
"stop" -> {
stop()
result.success(true)
}
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
if (flClashService is FlClashVpnService) {
(flClashService as FlClashVpnService).protect(fd)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"start" -> {
val data = call.argument<String>("data")
options = Gson().fromJson(data, VpnOptions::class.java)
when (options.enable) {
true -> handleStartVpn()
false -> start()
}
result.success(true)
} else {
result.success(false)
}
}
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
"stop" -> {
stop()
result.success(true)
}
else -> {
result.notImplemented()
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
if (flClashService is FlClashVpnService) {
(flClashService as FlClashVpnService).protect(fd)
}
result.success(true)
} else {
result.success(false)
}
}
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
"resolverProcess" -> {
val data = call.argument<String>("data")
val process =
if (data != null) Gson().fromJson(
data,
Process::class.java
) else null
val metadata = process?.metadata
if (metadata == null) {
result.success(null)
return
}
val protocol = metadata.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages = context.packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
else -> {
result.notImplemented()
}
}
}
@SuppressLint("ForegroundServiceType")
fun handleStartVpn() {
private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
start()
}
}
@SuppressLint("ForegroundServiceType")
fun requestGc() {
flutterMethodChannel.invokeMethod("gc", null)
}
val networks = mutableSetOf<Network>()
fun onUpdateNetwork() {
val dns = networks.flatMap { network ->
connectivity?.resolveDns(network) ?: emptyList()
}
.toSet()
.joinToString(",")
scope.launch {
withContext(Dispatchers.Main) {
flutterMethodChannel.invokeMethod("dnsChanged", dns)
}
}
// if (flClashService is FlClashVpnService) {
// val network = networks.maxByOrNull { net ->
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
// } ?: -1
// }
// network?.let {
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
// }
// }
}
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
networks.add(network)
onUpdateNetwork()
}
override fun onLost(network: Network) {
networks.remove(network)
onUpdateNetwork()
}
}
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
private fun registerNetworkCallback() {
networks.clear()
connectivity?.registerNetworkCallback(request, callback)
}
private fun unRegisterNetworkCallback() {
connectivity?.unregisterNetworkCallback(callback)
networks.clear()
onUpdateNetwork()
}
private fun startForeground(title: String, content: String) {
GlobalState.runLock.withLock {
if (GlobalState.runState.value != RunState.START) return
@@ -118,8 +237,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val fd = flClashService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
val fd = flClashService?.start(options)
flutterMethodChannel.invokeMethod(
"started",
fd
)
}
}
@@ -133,7 +255,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
private fun bindService() {
val intent = when (props?.enable == true) {
val intent = when (options.enable) {
true -> Intent(context, FlClashVpnService::class.java)
false -> Intent(context, FlClashService::class.java)
}

View File

@@ -11,14 +11,12 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.MainActivity
import com.follow.clash.models.Props
import com.follow.clash.models.VpnOptions
@SuppressLint("WrongConstant")
class FlClashService : Service(), BaseServiceInterface {
private val binder = LocalBinder()
@@ -73,7 +71,7 @@ class FlClashService : Service(), BaseServiceInterface {
}
}
override fun start(port: Int, props: Props?): Int? = null
override fun start(options: VpnOptions) = 0
override fun stop() {
stopSelf()

View File

@@ -1,5 +1,6 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
@@ -37,6 +38,7 @@ class FlClashTileService : TileService() {
GlobalState.runState.observeForever(observer)
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
@@ -67,15 +69,15 @@ class FlClashTileService : TileService() {
activityTransfer()
if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
val titlePlugin = GlobalState.getCurrentTitlePlugin()
if (titlePlugin != null) {
titlePlugin.handleStart()
val tilePlugin = GlobalState.getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
GlobalState.initServiceEngine(applicationContext)
}
} else if (GlobalState.runState.value == RunState.START) {
GlobalState.runState.value = RunState.PENDING
GlobalState.getCurrentTitlePlugin()?.handleStop()
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.Network
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
@@ -14,53 +15,41 @@ import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.TempActivity
import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.Props
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@SuppressLint("WrongConstant")
class FlClashVpnService : VpnService(), BaseServiceInterface {
private val passList = listOf(
"*zhihu.com",
"*zhimg.com",
"*jd.com",
"100ime-iat-api.xfyun.cn",
"*360buyimg.com",
"localhost",
"*.local",
"127.*",
"10.*",
"172.16.*",
"172.17.*",
"172.18.*",
"172.19.*",
"172.2*",
"172.30.*",
"172.31.*",
"192.168.*"
)
override fun onCreate() {
super.onCreate()
GlobalState.initServiceEngine(applicationContext)
}
override fun start(port: Int, props: Props?): Int? {
override fun start(options: VpnOptions): Int {
return with(Builder()) {
addAddress("172.16.0.1", 30)
if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
addRoute("0.0.0.0", 0)
}
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
addRoute("::", 0)
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)
addRoute("0.0.0.0", 0)
props?.accessControl?.let { accessControl ->
options.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
@@ -75,28 +64,33 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
}
}
}
addDnsServer("172.16.0.2")
setSession("FlClash")
setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
if (props?.allowBypass == true) {
if (options.allowBypass) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
port,
passList
options.port,
options.bypassDomain
)
)
}
establish()?.detachFd()
?: throw NullPointerException("Establish VPN rejected by system")
}
}
fun updateUnderlyingNetworks(networks: Array<Network>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
this.setUnderlyingNetworks(networks)
}
}
override fun stop() {
stopSelf()
@@ -127,6 +121,27 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
PendingIntent.FLAG_UPDATE_CURRENT
)
}
val stopIntent = Intent(this, TempActivity::class.java)
stopIntent.action = "com.follow.clash.action.STOP"
stopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val stopPendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
stopIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
@@ -140,6 +155,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
addAction(0, "Stop", stopPendingIntent);
}
}
@@ -165,7 +181,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentAppPlugin()?.requestGc()
GlobalState.getCurrentVPNPlugin()?.requestGc()
}
private val binder = LocalBinder()
@@ -178,7 +194,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTitlePlugin()?.handleStop()
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}
return isSuccess

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
assets/fonts/Icons.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -3,8 +3,12 @@ package main
import "C"
import (
"context"
"core/state"
"errors"
"math"
"fmt"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/hub/route"
"github.com/samber/lo"
"os"
"os/exec"
"path/filepath"
@@ -21,13 +25,11 @@ import (
"github.com/metacubex/mihomo/common/batch"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/sniffer"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
@@ -39,6 +41,7 @@ type ConfigExtendedParams struct {
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
}
type GenerateConfigParams struct {
@@ -154,6 +157,16 @@ func getRawConfigWithId(id string) *config.RawConfig {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
if configParams.TestURL != nil {
if mapping["health-check"] != nil {
hc := mapping["health-check"].(map[string]any)
if hc != nil {
if hc["url"] != nil {
hc["url"] = *configParams.TestURL
}
}
}
}
}
for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string)
@@ -190,7 +203,7 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
Path: psp.Vehicle().Path(),
UpdateAt: psp.UpdatedAt,
UpdateAt: psp.UpdatedAt(),
}, nil
case *rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
@@ -200,7 +213,7 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
VehicleType: rsp.VehicleType().String(),
Count: rsp.Count(),
Path: rsp.Vehicle().Path(),
UpdateAt: rsp.UpdatedAt,
UpdateAt: rsp.UpdatedAt(),
}, nil
default:
return nil, errors.New("not external provider")
@@ -211,16 +224,16 @@ func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
switch p.(type) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
elm, same, err := psp.SideUpdate(bytes)
if err == nil && !same {
psp.OnUpdate(elm)
_, _, err := psp.SideUpdate(bytes)
if err == nil {
return err
}
return nil
case rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
elm, same, err := rsp.SideUpdate(bytes)
if err == nil && !same {
rsp.OnUpdate(elm)
_, _, err := rsp.SideUpdate(bytes)
if err == nil {
return err
}
return nil
default:
@@ -234,150 +247,187 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof
}
func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
for _, v := range s {
initVal = f(initVal, v)
//func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
// for _, v := range s {
// initVal = f(initVal, v)
// }
// return initVal
//}
//
//func Map[T, U any](slice []T, fn func(T) U) []U {
// result := make([]U, len(slice))
// for i, v := range slice {
// result[i] = fn(v)
// }
// return result
//}
//
//func replaceFromMap(s string, m map[string]string) string {
// for k, v := range m {
// s = strings.ReplaceAll(s, k, v)
// }
// return s
//}
//
//func removeDuplicateFromSlice[T any](slice []T) []T {
// result := make([]T, 0)
// seen := make(map[any]struct{})
// for _, value := range slice {
// if _, ok := seen[value]; !ok {
// result = append(result, value)
// seen[value] = struct{}{}
// }
// }
// return result
//}
//func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
// var replacements = map[string]string{}
// var selectArr []map[string]any
// var urlTestArr []map[string]any
// var fallbackArr []map[string]any
// for _, group := range *proxyGroup {
// switch group["type"] {
// case "select":
// selectArr = append(selectArr, group)
// replacements[group["name"].(string)] = "Proxy"
// break
// case "url-test":
// urlTestArr = append(urlTestArr, group)
// replacements[group["name"].(string)] = "Auto"
// break
// case "fallback":
// fallbackArr = append(fallbackArr, group)
// replacements[group["name"].(string)] = "Fallback"
// break
// default:
// break
// }
// }
//
// ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Proxy" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
//
// AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Auto" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// AutoProxies = removeDuplicateFromSlice(AutoProxies)
//
// FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Fallback" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
//
// var computedProxyGroup []map[string]any
//
// if len(ProxyProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Proxy",
// "type": "select",
// "proxies": ProxyProxies,
// })
// }
//
// if len(AutoProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Auto",
// "type": "url-test",
// "proxies": AutoProxies,
// })
// }
//
// if len(FallbackProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Fallback",
// "type": "fallback",
// "proxies": FallbackProxies,
// })
// }
//
// computedRule := Map(*rule, func(value string) string {
// return replaceFromMap(value, replacements)
// })
//
// *proxyGroup = computedProxyGroup
// *rule = computedRule
//}
func genHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
hosts[k] = v
}
return initVal
}
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return result
return
}
func replaceFromMap(s string, m map[string]string) string {
for k, v := range m {
s = strings.ReplaceAll(s, k, v)
}
return s
}
var ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
func removeDuplicateFromSlice[T any](slice []T) []T {
result := make([]T, 0)
seen := make(map[any]struct{})
for _, value := range slice {
if _, ok := seen[value]; !ok {
result = append(result, value)
seen[value] = struct{}{}
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
rule := trimArr(strings.Split(line, ","))
l := len(rule)
if l != 2 {
return
}
}
return result
}
func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
var replacements = map[string]string{}
var selectArr []map[string]any
var urlTestArr []map[string]any
var fallbackArr []map[string]any
for _, group := range *proxyGroup {
switch group["type"] {
case "select":
selectArr = append(selectArr, group)
replacements[group["name"].(string)] = "Proxy"
break
case "url-test":
urlTestArr = append(urlTestArr, group)
replacements[group["name"].(string)] = "Auto"
break
case "fallback":
fallbackArr = append(fallbackArr, group)
replacements[group["name"].(string)] = "Fallback"
break
default:
if strings.ToUpper(rule[0]) == "MATCH" {
target = rule[1]
break
}
}
ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
if cur["proxies"] == nil {
return res
}
for _, proxyName := range cur["proxies"].([]interface{}) {
if str, ok := proxyName.(string); ok {
str = replaceFromMap(str, replacements)
if str != "Proxy" {
res = append(res, str)
}
}
}
return res
})
ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
if cur["proxies"] == nil {
return res
}
for _, proxyName := range cur["proxies"].([]interface{}) {
if str, ok := proxyName.(string); ok {
str = replaceFromMap(str, replacements)
if str != "Auto" {
res = append(res, str)
}
}
}
return res
})
AutoProxies = removeDuplicateFromSlice(AutoProxies)
FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
if cur["proxies"] == nil {
return res
}
for _, proxyName := range cur["proxies"].([]interface{}) {
if str, ok := proxyName.(string); ok {
str = replaceFromMap(str, replacements)
if str != "Fallback" {
res = append(res, str)
}
}
}
return res
})
FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
var computedProxyGroup []map[string]any
if len(ProxyProxies) > 0 {
computedProxyGroup = append(computedProxyGroup,
map[string]any{
"name": "Proxy",
"type": "select",
"proxies": ProxyProxies,
})
if target == "" {
return
}
if len(AutoProxies) > 0 {
computedProxyGroup = append(computedProxyGroup,
map[string]any{
"name": "Auto",
"type": "url-test",
"proxies": AutoProxies,
})
}
if len(FallbackProxies) > 0 {
computedProxyGroup = append(computedProxyGroup,
map[string]any{
"name": "Fallback",
"type": "fallback",
"proxies": FallbackProxies,
})
}
computedRule := Map(*rule, func(value string) string {
return replaceFromMap(value, replacements)
var rulesExt = lo.Map(ips, func(ip string, index int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target)
})
*proxyGroup = computedProxyGroup
*rule = computedRule
*rules = append(rulesExt, *rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -387,7 +437,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.ExternalUIURL = ""
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
//targetConfig.GeodataMode = false
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
@@ -405,27 +454,36 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA
if targetConfig.DNS.Enable == false {
targetConfig.DNS = patchConfig.DNS
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
}
overrideRules(&targetConfig.Rule)
//if runtime.GOOS == "android" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
//} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//}
if configParams.IsCompatible == false {
targetConfig.ProxyProvider = make(map[string]map[string]any)
targetConfig.RuleProvider = make(map[string]map[string]any)
generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
}
//if configParams.IsCompatible == false {
// targetConfig.ProxyProvider = make(map[string]map[string]any)
// targetConfig.RuleProvider = make(map[string]map[string]any)
// generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
//}
}
func patchConfig(general *config.General) {
func patchConfig(general *config.General, controller *config.Controller, tls *config.TLS) {
log.Infoln("[Apply] patch")
route.ReStartServer(general.ExternalController)
if sniffer.Dispatcher != nil {
tunnel.SetSniffing(general.Sniffing)
}
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
@@ -433,6 +491,22 @@ func patchConfig(general *config.General) {
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
route.ReCreateServer(&route.Config{
Addr: controller.ExternalController,
TLSAddr: controller.ExternalControllerTLS,
UnixAddr: controller.ExternalControllerUnix,
PipeAddr: controller.ExternalControllerPipe,
Secret: controller.Secret,
Certificate: tls.Certificate,
PrivateKey: tls.PrivateKey,
DohServer: controller.ExternalDohServer,
IsDebug: false,
Cors: route.Cors{
AllowOrigins: controller.Cors.AllowOrigins,
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
},
})
}
var isRunning = false
@@ -440,6 +514,12 @@ var isRunning = false
var runLock sync.Mutex
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
if !isRunning {
return
}
runLock.Lock()
defer runLock.Unlock()
stopListeners()
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -449,41 +529,20 @@ func updateListeners(general *config.General, listeners map[string]constant.Inbo
listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateRedirToTun(general.EBpf.RedirectToTun)
if !features.Android {
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
}
}
func stopListeners() {
listener.StopListener()
}
func hcCompatibleProvider(proxyProviders map[string]cp.ProxyProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, math.MaxInt)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == cp.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
}
}()
}
}
}
func patchSelectGroup() {
mapping := configParams.SelectedMap
if mapping == nil {
@@ -510,25 +569,19 @@ func patchSelectGroup() {
}
func applyConfig() error {
cfg, err := config.ParseRawConfig(currentRawConfig)
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
if configParams.IsPatch {
patchConfig(cfg.General)
patchConfig(cfg.General, cfg.Controller, cfg.TLS)
} else {
closeConnections()
runtime.GC()
hub.UltraApplyConfig(cfg)
hub.ApplyConfig(cfg)
patchSelectGroup()
}
if isRunning {
updateListeners(cfg.General, cfg.Listeners)
hcCompatibleProvider(cfg.Providers)
}
updateListeners(cfg.General, cfg.Listeners)
externalProviders = getExternalProvidersRaw()
return err
}

20
core/dns.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build android
package main
import "C"
import (
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/log"
"strings"
)
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
go func() {
log.Infoln("[DNS] updateDns %s", dnsList)
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
dns.FlushCacheWithDefaultResolver()
}()
}

View File

@@ -4,14 +4,18 @@ go 1.21.0
replace github.com/metacubex/mihomo => ./Clash.Meta
require github.com/metacubex/mihomo v1.17.1
require (
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
github.com/metacubex/mihomo v1.17.1
github.com/miekg/dns v1.1.61
golang.org/x/net v0.26.0
golang.org/x/sync v0.7.0
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
)
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
require (
github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
@@ -20,30 +24,28 @@ require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cilium/ebpf v0.12.3 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.7.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.0.14 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.2.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
@@ -52,19 +54,21 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
github.com/metacubex/sing-shadowsocks v0.2.7 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.1 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -72,10 +76,9 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
@@ -83,8 +86,7 @@ require (
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/samber/lo v1.47.0
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
@@ -101,14 +103,16 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
lukechampine.com/blake3 v1.3.0 // indirect
)

View File

@@ -1,7 +1,5 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34 h1:USCTqih5d1bUXUxWNS9ZD5Tx/lb0jXHEtRIIx/F9dMc=
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34/go.mod h1:YR9wK13TgI5ww8iKWm91MHiSoHC7Oz0U4beCCmtXqLw=
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A=
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
@@ -19,17 +17,15 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -40,16 +36,12 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -65,8 +57,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -82,8 +74,8 @@ github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7s
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 h1:dh8D8FksyMhD64mRMbUhZHWYJfNoNMCxfVq6eexleMw=
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -92,10 +84,6 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -106,34 +94,42 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvWDcBDAkIv5kUYIhzHwafDVq635BuybnKqI=
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g=
github.com/metacubex/sing-shadowsocks v0.2.7/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg=
github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog=
github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y=
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
@@ -156,35 +152,28 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.0-alpha.13 h1:fpR4TFZfu/9V3LbHSAnnnwcaXGMF8ijmAAPoY2WHSKw=
github.com/sagernet/sing v0.5.0-alpha.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@@ -200,9 +189,15 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -216,6 +211,10 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -230,21 +229,21 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -260,19 +259,19 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=

View File

@@ -5,9 +5,12 @@ package main
*/
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"os"
"runtime"
"sort"
@@ -18,7 +21,6 @@ import (
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
@@ -27,11 +29,8 @@ import (
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context"
)
var currentRawConfig = config.DefaultRawConfig()
var configParams = ConfigExtendedParams{}
var externalProviders = map[string]cp.Provider{}
@@ -48,9 +47,11 @@ func start() {
//export stop
func stop() {
runLock.Lock()
defer runLock.Unlock()
isRunning = false
stopListeners()
go func() {
defer runLock.Unlock()
isRunning = false
stopListeners()
}()
}
//export initClash
@@ -122,7 +123,7 @@ func updateConfig(s *C.char, port C.longlong) {
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
currentRawConfig = prof
state.CurrentRawConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
@@ -182,7 +183,7 @@ func changeProxy(s *C.char) {
//export getTraffic
func getTraffic() *C.char {
up, down := statistic.DefaultManager.Current(state.OnlyProxy)
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -197,7 +198,7 @@ func getTraffic() *C.char {
//export getTotalTraffic
func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total(state.OnlyProxy)
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -223,11 +224,13 @@ func asyncTestDelay(s *C.char, port C.longlong) {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
bridge.SendToPort(i, "")
return false, nil
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
bridge.SendToPort(i, "")
return false, nil
}
@@ -374,27 +377,28 @@ func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
geoTypeString := C.GoString(geoType)
geoNameString := C.GoString(geoName)
go func() {
path := constant.Path.Resolve(geoNameString)
switch geoTypeString {
case "MMDB":
err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
err := updater.UpdateMMDBWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
err := updater.UpdateASNWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
err := updater.UpdateGeoIpWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
err := updater.UpdateGeoSiteWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return

View File

@@ -2,36 +2,33 @@ package main
import "C"
import (
"core/state"
"encoding/json"
"fmt"
)
type AccessControl struct {
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
if state.CurrentState == nil {
return C.CString("")
}
return C.CString(state.CurrentState.CurrentProfileName)
}
type AndroidProps struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
}
type State struct {
AndroidProps
CurrentProfileName string `json:"currentProfileName"`
MixedPort int `json:"mixedPort"`
OnlyProxy bool `json:"onlyProxy"`
}
var state State
//export getState
func getState() *C.char {
data, err := json.Marshal(state)
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Port: state.CurrentRawConfig.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
@@ -42,7 +39,7 @@ func getState() *C.char {
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), &state)
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
if err != nil {
return
}

59
core/state/state.go Normal file
View File

@@ -0,0 +1,59 @@
package state
import "github.com/metacubex/mihomo/config"
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
var CurrentRawConfig = config.DefaultRawConfig()
type AndroidVpnOptions struct {
Enable bool `json:"enable"`
Port int `json:"port"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"`
Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"`
}
type AccessControl struct {
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
}
type AndroidVpnRawOptions struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
}
type State struct {
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
OnlyProxy bool `json:"onlyProxy"`
}
var CurrentState = &State{}
func GetIpv6Address() string {
if CurrentState.Ipv6 {
return DefaultIpv6Address
} else {
return ""
}
}
func GetDnsServerAddress() string {
//prefix, _ := netip.ParsePrefix(DefaultIpv4Address)
//return prefix.Addr().String()
return DefaultDnsAddress
}

View File

@@ -7,6 +7,7 @@ import (
"core/platform"
t "core/tun"
"errors"
"github.com/metacubex/mihomo/listener/sing_tun"
"strconv"
"sync"
"sync/atomic"
@@ -15,11 +16,9 @@ import (
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
)
var tunLock sync.Mutex
var tun *t.Tun
var runTime *time.Time
type FdMap struct {
@@ -35,7 +34,11 @@ func (cm *FdMap) Load(key int64) bool {
return ok
}
var fdMap FdMap
var (
tunListener *sing_tun.Listener
fdMap FdMap
fdCounter int64 = 0
)
//export startTUN
func startTUN(fd C.int, port C.longlong) {
@@ -56,30 +59,12 @@ func startTUN(fd C.int, port C.longlong) {
go func() {
tunLock.Lock()
defer tunLock.Unlock()
if tun != nil {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
dns := "0.0.0.0"
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
closer, err := t.Start(f, gateway, portal, dns)
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
tunListener, _ = t.Start(f)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
tempTun.Closer = closer
tun = tempTun
now := time.Now()
runTime = &now
@@ -108,10 +93,8 @@ func stopTun() {
runTime = nil
if tun != nil {
log.Errorln("[Tun] stopTun")
tun.Close()
tun = nil
if tunListener != nil {
_ = tunListener.Close()
}
}()
}
@@ -138,18 +121,12 @@ func markSocket(fd Fd) {
})
}
var fdCounter int64 = 0
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
if tun == nil {
return
}
fdInt := int64(fd)
timeout := time.After(100 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)

View File

@@ -1,33 +0,0 @@
//go:build android
package tun
import (
"github.com/metacubex/mihomo/dns"
D "github.com/miekg/dns"
"net"
)
func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
if targetPort != 53 {
return false
}
return net.IPv4zero.Equal(dns) || target.Equal(dns)
}
func relayDns(payload []byte) ([]byte, error) {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return nil, err
}
r, err := dns.ServeDNSWithDefaultServer(msg)
if err != nil {
return nil, err
}
r.SetRcode(msg, r.Rcode)
return r.Pack()
}

View File

@@ -1,20 +0,0 @@
//go:build android
package tun
import (
"github.com/metacubex/mihomo/constant"
"net"
)
func createMetadata(lAddr, rAddr *net.TCPAddr) *constant.Metadata {
return &constant.Metadata{
NetWork: constant.TCP,
Type: constant.SOCKS5,
SrcIP: lAddr.AddrPort().Addr(),
DstIP: rAddr.AddrPort().Addr(),
SrcPort: uint16(lAddr.Port),
DstPort: uint16(rAddr.Port),
Host: "",
}
}

View File

@@ -4,183 +4,65 @@ package tun
import "C"
import (
"context"
"encoding/binary"
"github.com/Kr328/tun2socket"
"github.com/Kr328/tun2socket/nat"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/constant"
"core/state"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/tunnel"
"golang.org/x/sync/semaphore"
"io"
"net"
"os"
"time"
"net/netip"
)
type Tun struct {
Closer io.Closer
Closed bool
Limit *semaphore.Weighted
type Props struct {
Fd int `json:"fd"`
Gateway string `json:"gateway"`
Gateway6 string `json:"gateway6"`
Portal string `json:"portal"`
Portal6 string `json:"portal6"`
Dns string `json:"dns"`
Dns6 string `json:"dns6"`
}
func (t *Tun) Close() {
_ = t.Limit.Acquire(context.TODO(), 4)
defer t.Limit.Release(4)
t.Closed = true
if t.Closer != nil {
_ = t.Closer.Close()
}
}
var _, ipv4LoopBack, _ = net.ParseCIDR("127.0.0.0/8")
func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
device := os.NewFile(uintptr(fd), "/dev/tun")
ip, network, err := net.ParseCIDR(gateway)
func Start(fd int) (*sing_tun.Listener, error) {
var prefix4 []netip.Prefix
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
if err != nil {
panic(err.Error())
} else {
network.IP = ip
log.Errorln("startTUN error:", err)
return nil, err
}
stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal))
prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix
if state.CurrentState.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil {
log.Errorln("startTUN error:", err)
return nil, err
}
prefix6 = append(prefix6, tempPrefix6)
}
var dnsHijack []string
dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53"))
options := LC.Tun{
Enable: true,
Device: state.CurrentRawConfig.Tun.Device,
Stack: state.CurrentRawConfig.Tun.Stack,
DNSHijack: dnsHijack,
AutoRoute: false,
AutoDetectInterface: false,
Inet4Address: prefix4,
Inet6Address: prefix6,
MTU: 9000,
FileDescriptor: fd,
}
listener, err := sing_tun.New(options, tunnel.Tunnel)
if err != nil {
_ = device.Close()
log.Errorln("startTUN error:", err)
return nil, err
}
dnsAddr := net.ParseIP(dns)
tcp := func() {
defer func(tcp *nat.TCP) {
_ = tcp.Close()
}(stack.TCP())
defer log.Debugln("TCP: closed")
for stack.TCP().SetDeadline(time.Time{}) == nil {
conn, err := stack.TCP().Accept()
if err != nil {
log.Errorln("Accept connection: %v", err)
continue
}
lAddr := conn.LocalAddr().(*net.TCPAddr)
rAddr := conn.RemoteAddr().(*net.TCPAddr)
if ipv4LoopBack.Contains(rAddr.IP) {
_ = conn.Close()
continue
}
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
go func() {
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
buf := pool.Get(pool.UDPBufferSize)
defer func(buf []byte) {
_ = pool.Put(buf)
}(buf)
for {
_ = conn.SetReadDeadline(time.Now().Add(constant.DefaultTCPTimeout))
length := uint16(0)
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return
}
if int(length) > len(buf) {
return
}
n, err := conn.Read(buf[:length])
if err != nil {
return
}
msg, err := relayDns(buf[:n])
if err != nil {
return
}
_, _ = conn.Write(msg)
}
}()
continue
}
go tunnel.Tunnel.HandleTCPConn(conn, createMetadata(lAddr, rAddr))
}
}
udp := func() {
defer func(udp *nat.UDP) {
_ = udp.Close()
}(stack.UDP())
defer log.Debugln("UDP: closed")
for {
buf := pool.Get(pool.UDPBufferSize)
n, lRAddr, rRAddr, err := stack.UDP().ReadFrom(buf)
if err != nil {
return
}
raw := buf[:n]
lAddr := lRAddr.(*net.UDPAddr)
rAddr := rRAddr.(*net.UDPAddr)
if ipv4LoopBack.Contains(rAddr.IP) {
_ = pool.Put(buf)
continue
}
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
go func() {
defer func(buf []byte) {
_ = pool.Put(buf)
}(buf)
msg, err := relayDns(raw)
if err != nil {
return
}
_, _ = stack.UDP().WriteTo(msg, rAddr, lAddr)
}()
continue
}
pkt := &packet{
local: lAddr,
data: raw,
writeBack: func(b []byte, addr net.Addr) (int, error) {
return stack.UDP().WriteTo(b, addr, lAddr)
},
drop: func() {
_ = pool.Put(buf)
},
}
tunnel.Tunnel.HandleUDPPacket(inbound.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, constant.SOCKS5))
}
}
go tcp()
go udp()
return stack, nil
return listener, nil
}

View File

@@ -1,28 +0,0 @@
//go:build android
package tun
import "net"
type packet struct {
local *net.UDPAddr
data []byte
writeBack func(b []byte, addr net.Addr) (int, error)
drop func()
}
func (pkt *packet) Data() []byte {
return pkt.data
}
func (pkt *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) {
return pkt.writeBack(b, addr)
}
func (pkt *packet) Drop() {
pkt.drop()
}
func (pkt *packet) LocalAddr() net.Addr {
return pkt.local
}

View File

@@ -1,11 +1,10 @@
import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/proxy_container.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
@@ -28,11 +27,13 @@ runAppWithPreferences(
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => AppFlowingState(),
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.isCompatible = config.isCompatible;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
@@ -53,6 +54,7 @@ class Application extends StatefulWidget {
class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes;
Timer? timer;
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
@@ -81,7 +83,9 @@ class ApplicationState extends State<Application> {
@override
void initState() {
super.initState();
_initTimer();
globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) {
@@ -92,23 +96,52 @@ class ApplicationState extends State<Application> {
});
}
_initTimer() {
_cancelTimer();
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateGroupDebounce();
});
});
}
_cancelTimer() {
if (timer != null) {
timer?.cancel();
timer = null;
}
}
_buildApp(Widget app) {
if (system.isDesktop) {
return WindowContainer(
child: TrayContainer(
child: ProxyContainer(
child: app,
return WindowManager(
child: TrayManager(
child: HotKeyManager(
child: ProxyManager(
child: app,
),
),
),
);
}
return AndroidContainer(
child: TileContainer(
return AndroidManager(
child: TileManager(
child: app,
),
);
}
_buildPage(Widget page) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnManager(
child: page,
);
}
_updateSystemColorSchemes(
ColorScheme? lightDynamic,
ColorScheme? darkDynamic,
@@ -125,14 +158,15 @@ class ApplicationState extends State<Application> {
@override
Widget build(context) {
return _buildApp(
AppStateContainer(
child: ClashContainer(
AppStateManager(
child: ClashManager(
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
prueBlack: config.prueBlack,
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(
@@ -147,10 +181,16 @@ class ApplicationState extends State<Application> {
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
if (system.isDesktop) {
return WindowHeaderContainer(child: child!);
}
return child!;
return LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!);
},
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
@@ -160,6 +200,7 @@ class ApplicationState extends State<Application> {
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
@@ -169,6 +210,7 @@ class ApplicationState extends State<Application> {
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
@@ -193,5 +235,6 @@ class ApplicationState extends State<Application> {
linkManager.destroy();
await globalState.appController.savePreferences();
super.dispose();
_cancelTimer();
}
}

View File

@@ -158,7 +158,7 @@ class ClashCore {
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if(externalProviderRawString.isEmpty) return null;
if (externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
}
@@ -287,13 +287,18 @@ class ClashCore {
malloc.free(stateChar);
}
CoreState getState() {
final stateRaw = clashFFI.getState();
final state = json.decode(
stateRaw.cast<Utf8>().toDartString(),
);
clashFFI.freeCString(stateRaw);
return CoreState.fromJson(state);
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
Traffic getTraffic() {
@@ -327,6 +332,13 @@ class ClashCore {
clashFFI.startTUN(fd, port);
}
updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
requestGc() {
clashFFI.forceGc();
}

View File

@@ -5144,6 +5144,20 @@ class ClashFFI {
late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
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 start() {
return _start();
}
@@ -5543,14 +5557,25 @@ class ClashFFI {
late final _setProcessMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getState() {
return _getState();
ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName();
}
late final _getStatePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('getState');
late final _getState =
_getStatePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
late final _getCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getCurrentProfileName');
late final _getCurrentProfileName =
_getCurrentProfileNamePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getAndroidVpnOptions() {
return _getAndroidVpnOptions();
}
late final _getAndroidVpnOptionsPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getAndroidVpnOptions');
late final _getAndroidVpnOptions =
_getAndroidVpnOptionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void setState(
ffi.Pointer<ffi.Char> s,

View File

@@ -1,10 +1,13 @@
import 'dart:io';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
class Android {
init() async {
app?.onExit = () {};
app?.onExit = () async {
await globalState.appController.savePreferences();
};
}
}

View File

@@ -25,4 +25,8 @@ export 'package.dart';
export 'measure.dart';
export 'windows.dart';
export 'iterable.dart';
export 'scroll.dart';
export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';
export 'network.dart';

View File

@@ -1,7 +1,9 @@
import 'dart:io';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'system.dart';
@@ -16,14 +18,18 @@ const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop ? 40 : 0;
final double kHeaderHeight = system.isDesktop
? !Platform.isMacOS
? 40
: 26
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
@@ -46,6 +52,21 @@ final filter = ImageFilter.blur(
tileMode: TileMode.mirror,
);
const navigationItemListEquality = ListEquality<NavigationItem>();
const connectionListEquality = ListEquality<Connection>();
const stringListEquality = ListEquality<String>();
const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>();
const externalProviderListEquality = ListEquality<ExternalProvider>();
const packageListEquality = ListEquality<Package>();
const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>();
const stringAndIntQMapEquality = MapEquality<String, int?>();
const stringSetEquality = SetEquality<String>();
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
const viewModeColumnsMap = {
ViewMode.mobile: [2, 1],
ViewMode.laptop: [3, 2],

View File

@@ -11,7 +11,7 @@ extension BuildContextExtension on BuildContext {
return MediaQuery.of(this).size;
}
double get width {
double get viewWidth {
return appSize.width;
}

View File

@@ -8,6 +8,7 @@ import 'package:webdav_client/webdav_client.dart';
class DAVClient {
late Client client;
Completer<bool> pingCompleter = Completer();
late String fileName;
DAVClient(DAV dav) {
client = newClient(
@@ -15,6 +16,7 @@ class DAVClient {
user: dav.user,
password: dav.password,
);
fileName = dav.fileName;
client.setHeaders(
{
'accept-charset': 'utf-8',
@@ -38,7 +40,7 @@ class DAVClient {
get root => "/$appName";
get backupFile => "$root/backup.zip";
get backupFile => "$root/$fileName";
backup(Uint8List data) async {
await client.mkdir("$root");

22
lib/common/http.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
class FlClashHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
debugPrint("find $url");
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
return client;
}
}

6
lib/common/icons.dart Normal file
View File

@@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
class IconsExt{
static const IconData target =
IconData(0xe900, fontFamily: "Icons");
}

View File

@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
}
}
return -1; // 这行理论上不会执行到,但为了完整性保留
return -1;
}
}

106
lib/common/keyboard.dart Normal file
View File

@@ -0,0 +1,106 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:uni_platform/uni_platform.dart';
final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
<PhysicalKeyboardKey, String>{
PhysicalKeyboardKey.keyA: 'A',
PhysicalKeyboardKey.keyB: 'B',
PhysicalKeyboardKey.keyC: 'C',
PhysicalKeyboardKey.keyD: 'D',
PhysicalKeyboardKey.keyE: 'E',
PhysicalKeyboardKey.keyF: 'F',
PhysicalKeyboardKey.keyG: 'G',
PhysicalKeyboardKey.keyH: 'H',
PhysicalKeyboardKey.keyI: 'I',
PhysicalKeyboardKey.keyJ: 'J',
PhysicalKeyboardKey.keyK: 'K',
PhysicalKeyboardKey.keyL: 'L',
PhysicalKeyboardKey.keyM: 'M',
PhysicalKeyboardKey.keyN: 'N',
PhysicalKeyboardKey.keyO: 'O',
PhysicalKeyboardKey.keyP: 'P',
PhysicalKeyboardKey.keyQ: 'Q',
PhysicalKeyboardKey.keyR: 'R',
PhysicalKeyboardKey.keyS: 'S',
PhysicalKeyboardKey.keyT: 'T',
PhysicalKeyboardKey.keyU: 'U',
PhysicalKeyboardKey.keyV: 'V',
PhysicalKeyboardKey.keyW: 'W',
PhysicalKeyboardKey.keyX: 'X',
PhysicalKeyboardKey.keyY: 'Y',
PhysicalKeyboardKey.keyZ: 'Z',
PhysicalKeyboardKey.digit1: '1',
PhysicalKeyboardKey.digit2: '2',
PhysicalKeyboardKey.digit3: '3',
PhysicalKeyboardKey.digit4: '4',
PhysicalKeyboardKey.digit5: '5',
PhysicalKeyboardKey.digit6: '6',
PhysicalKeyboardKey.digit7: '7',
PhysicalKeyboardKey.digit8: '8',
PhysicalKeyboardKey.digit9: '9',
PhysicalKeyboardKey.digit0: '0',
PhysicalKeyboardKey.enter: 'ENTER',
PhysicalKeyboardKey.escape: 'ESCAPE',
PhysicalKeyboardKey.backspace: 'BACKSPACE',
PhysicalKeyboardKey.tab: 'TAB',
PhysicalKeyboardKey.space: 'SPACE',
PhysicalKeyboardKey.minus: '-',
PhysicalKeyboardKey.equal: '=',
PhysicalKeyboardKey.bracketLeft: '[',
PhysicalKeyboardKey.bracketRight: ']',
PhysicalKeyboardKey.backslash: '\\',
PhysicalKeyboardKey.semicolon: ';',
PhysicalKeyboardKey.quote: '"',
PhysicalKeyboardKey.backquote: '`',
PhysicalKeyboardKey.comma: ',',
PhysicalKeyboardKey.period: '.',
PhysicalKeyboardKey.slash: '/',
PhysicalKeyboardKey.capsLock: 'CAPSLOCK',
PhysicalKeyboardKey.f1: 'F1',
PhysicalKeyboardKey.f2: 'F2',
PhysicalKeyboardKey.f3: 'F3',
PhysicalKeyboardKey.f4: 'F4',
PhysicalKeyboardKey.f5: 'F5',
PhysicalKeyboardKey.f6: 'F6',
PhysicalKeyboardKey.f7: 'F7',
PhysicalKeyboardKey.f8: 'F8',
PhysicalKeyboardKey.f9: 'F9',
PhysicalKeyboardKey.f10: 'F10',
PhysicalKeyboardKey.f11: 'F11',
PhysicalKeyboardKey.f12: 'F12',
PhysicalKeyboardKey.home: 'HOME',
PhysicalKeyboardKey.pageUp: 'PAGEUP',
PhysicalKeyboardKey.delete: 'DELETE',
PhysicalKeyboardKey.end: 'END',
PhysicalKeyboardKey.pageDown: 'PAGEDOWN',
PhysicalKeyboardKey.arrowRight: '',
PhysicalKeyboardKey.arrowLeft: '',
PhysicalKeyboardKey.arrowDown: '',
PhysicalKeyboardKey.arrowUp: '',
PhysicalKeyboardKey.controlLeft: "CTRL",
PhysicalKeyboardKey.shiftLeft: 'SHIFT',
PhysicalKeyboardKey.altLeft: "ALT",
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.controlRight: "CTRL",
PhysicalKeyboardKey.shiftRight: 'SHIFT',
PhysicalKeyboardKey.altRight: "ALT",
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.fn: 'FN',
};
extension KeyboardKeyExt on KeyboardKey {
String get label {
PhysicalKeyboardKey? physicalKey;
if (this is LogicalKeyboardKey) {
physicalKey = (this as LogicalKeyboardKey).physicalKey;
} else if (this is PhysicalKeyboardKey) {
physicalKey = this as PhysicalKeyboardKey;
}
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
}
}

View File

@@ -58,22 +58,7 @@ class AutoLaunch {
Future<bool> windowsEnable() async {
await disable();
return windows?.runas(
'schtasks',
[
'/Create',
'/SC',
'ONLOGON',
'/TN',
appName,
'/TR',
Platform.resolvedExecutable,
'/RL',
'HIGHEST',
'/F'
].join(" "),
) ??
false;
return await windows?.registerTask(appName) ?? false;
}
Future<bool> disable() async {
@@ -81,9 +66,9 @@ class AutoLaunch {
}
updateStatus(AutoLaunchState state) async {
final isOpenTun = state.isOpenTun;
final isAdminAutoLaunch = state.isAdminAutoLaunch;
final isAutoLaunch = state.isAutoLaunch;
if (Platform.isWindows && isOpenTun) {
if (Platform.isWindows && isAdminAutoLaunch) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();

View File

@@ -2,4 +2,23 @@ extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start) {
if(start <= 0) return this;
if(start > length) return [];
return sublist(start);
}
}

View File

@@ -3,24 +3,26 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Measure {
Measure.of(this.context);
final TextScaler _textScale;
late BuildContext context;
final _textScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
Measure.of(this.context)
: _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
);
Size computeTextSize(Text text) {
final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style),
maxLines: text.maxLines,
textScaler: TextScaler.linear(_textScaleFactor),
textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr,
)..layout();
return textPainter.size;
}
late BuildContext context;
double? _bodyMediumHeight;
Size? _bodyLargeSize;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
@@ -30,17 +32,27 @@ class Measure {
double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize(
Text(
"",
"X",
style: context.textTheme.bodyMedium,
),
).height;
return _bodyMediumHeight!;
}
Size get bodyLargeSize {
_bodyLargeSize ??= computeTextSize(
Text(
"X",
style: context.textTheme.bodyLarge,
),
);
return _bodyLargeSize!;
}
double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize(
Text(
"",
"X",
style: context.textTheme.bodySmall,
),
).height;
@@ -50,7 +62,7 @@ class Measure {
double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize(
Text(
"",
"X",
style: context.textTheme.labelSmall,
),
).height;
@@ -60,7 +72,7 @@ class Measure {
double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize(
Text(
"",
"X",
style: context.textTheme.labelMedium,
),
).height;
@@ -70,7 +82,7 @@ class Measure {
double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize(
Text(
"",
"X",
style: context.textTheme.titleLarge,
),
).height;
@@ -80,7 +92,7 @@ class Measure {
double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize(
Text(
"",
"X",
style: context.textTheme.titleMedium,
),
).height;

25
lib/common/network.dart Normal file
View File

@@ -0,0 +1,25 @@
import 'dart:io';
extension NetworkInterfaceExt on NetworkInterface {
bool get isWifi {
final nameLowCase = name.toLowerCase();
if (nameLowCase.contains('wlan') ||
nameLowCase.contains('wi-fi') ||
nameLowCase == 'en0' ||
nameLowCase == 'eth0') {
return true;
}
return false;
}
bool get includesIPv4 {
return addresses.any((addr) => addr.isIPv4);
}
}
extension InternetAddressExt on InternetAddress {
bool get isIPv4 {
return type == InternetAddressType.IPv4;
}
}

View File

@@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
@@ -99,12 +100,21 @@ class Other {
}
}
String getTrayIconPath() {
if (Platform.isWindows) {
return "assets/images/icon.ico";
} else {
return "assets/images/icon_monochrome.png";
String getTrayIconPath({
required bool isStart,
required Brightness brightness,
}) {
if(Platform.isMacOS){
return "assets/images/icon_white.png";
}
final suffix = Platform.isWindows ? "ico" : "png";
if (isStart && Platform.isWindows) {
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) {
@@ -130,6 +140,12 @@ class Other {
return build1.compareTo(build2);
}
String getPinyin(String value) {
return value.isNotEmpty
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
: "";
}
Future<String?> parseQRCode(Uint8List? bytes) {
return Isolate.run<String?>(() {
if (bytes == null) return null;
@@ -158,14 +174,18 @@ class Other {
if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition);
final parameters = parseValue.parameters;
final key = parameters.keys
.firstWhere((key) => key.startsWith("filename"), orElse: () => '');
if (key.isEmpty) return null;
if (key == "filename*") {
return Uri.decodeComponent((parameters[key] ?? "").split("'").last);
} else {
return parameters[key];
final fileNamePointKey = parameters.keys
.firstWhere((key) => key == "filename*", orElse: () => "");
if (fileNamePointKey.isNotEmpty) {
final res = parameters[fileNamePointKey]?.split("''") ?? [];
if (res.length >= 2) {
return Uri.decodeComponent(res[1]);
}
}
final fileNameKey = parameters.keys
.firstWhere((key) => key == "filename", orElse: () => "");
if (fileNameKey.isEmpty) return null;
return parameters[fileNameKey];
}
double getViewWidth() {
@@ -176,7 +196,7 @@ class Other {
List<String> parseReleaseBody(String? body) {
if (body == null) return [];
const pattern = r'- (.+?)\. \[.+?\]';
const pattern = r'- \s*(.*)';
final regex = RegExp(pattern);
return regex
.allMatches(body)
@@ -194,9 +214,9 @@ class Other {
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final columns = max((viewWidth / 300).ceil(), 2);
return switch (proxiesLayout) {
ProxiesLayout.tight => columns - 1,
ProxiesLayout.tight => columns + 1,
ProxiesLayout.standard => columns,
ProxiesLayout.loose => columns + 1,
ProxiesLayout.loose => columns - 1,
};
}
@@ -207,6 +227,15 @@ class Other {
String getBackupFileName() {
return "${appName}_backup_${DateTime.now().show}.zip";
}
String get logFile {
return "${appName}_${DateTime.now().show}.log";
}
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
}
final other = Other();

View File

@@ -8,11 +8,12 @@ import 'constant.dart';
class AppPath {
static AppPath? _instance;
Completer<Directory> cacheDir = Completer();
Completer<Directory> dataDir = Completer();
Completer<Directory> downloadDir = Completer();
Completer<Directory> tempDir = Completer();
late String appDirPath;
// Future<Directory> _createDesktopCacheDir() async {
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
// final dir = Directory(path);
// if (await dir.exists()) {
// await dir.create(recursive: true);
@@ -21,8 +22,12 @@ class AppPath {
// }
AppPath._internal() {
appDirPath = join(dirname(Platform.resolvedExecutable));
getApplicationSupportDirectory().then((value) {
cacheDir.complete(value);
dataDir.complete(value);
});
getTemporaryDirectory().then((value){
tempDir.complete(value);
});
getDownloadsDirectory().then((value) {
downloadDir.complete(value);
@@ -49,12 +54,12 @@ class AppPath {
}
Future<String> getHomeDirPath() async {
final directory = await cacheDir.future;
final directory = await dataDir.future;
return directory.path;
}
Future<String> getProfilesPath() async {
final directory = await cacheDir.future;
final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName);
}
@@ -63,6 +68,11 @@ class AppPath {
final directory = await getProfilesPath();
return join(directory, "$id.yaml");
}
Future<String> get tempPath async {
final directory = await tempDir.future;
return directory.path;
}
}
final appPath = AppPath();

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart';
@@ -28,7 +29,8 @@ class Preferences {
try {
return ClashConfig.fromJson(clashConfigMap);
} catch (e) {
throw e.toString();
debugPrint(e.toString());
return null;
}
}
@@ -48,7 +50,8 @@ class Preferences {
try {
return Config.fromJson(configMap);
} catch (e) {
throw e.toString();
debugPrint(e.toString());
return null;
}
}

View File

@@ -1,50 +1,26 @@
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/ip.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class Request {
late final Dio _dio;
int? _port;
bool _isStart = false;
String? userAgent;
Request() {
_dio = Dio();
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
_updateAdapter();
return handler.next(options); // 继续请求
},
),
);
}
_updateAdapter() {
final port = globalState.appController.clashConfig.mixedPort;
final isStart = globalState.appController.appState.isStart;
if (_port != port || isStart != _isStart) {
_port = port;
_isStart = isStart;
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
if (!_isStart) return client;
client.userAgent = globalState.appController.clashConfig.globalUa;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
return client;
},
validateCertificate: (_, __, ___) => true,
);
}
}
Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio
.get(
@@ -62,6 +38,19 @@ class Request {
return response;
}
Future<MemoryImage?> getImage(String url) async {
if (url.isEmpty) return null;
final response = await _dio.get<Uint8List>(
url,
options: Options(
responseType: ResponseType.bytes,
),
);
final data = response.data;
if (data == null) return null;
return MemoryImage(data);
}
Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await _dio.get(
"https://api.github.com/repos/$repository/releases/latest",
@@ -87,7 +76,7 @@ class Request {
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<Map<String, dynamic>>(
@@ -101,7 +90,7 @@ class Request {
return source.value(response.data!);
}
} catch (e) {
if(cancelToken?.isCancelled == true){
if (cancelToken?.isCancelled == true) {
throw "cancelled";
}
continue;

View File

@@ -25,3 +25,18 @@ class HiddenBarScrollBehavior extends BaseScrollBehavior {
return child;
}
}
class ShowBarScrollBehavior extends BaseScrollBehavior {
@override
Widget buildScrollbar(
BuildContext context,
Widget child,
ScrollableDetails details,
) {
return Scrollbar(
interactive: true,
controller: details.controller,
child: child,
);
}
}

View File

@@ -1,3 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
extension StringExtension on String {
bool get isUrl {
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
@@ -8,4 +13,38 @@ extension StringExtension on String {
other.toLowerCase(),
);
}
List<int> get encodeUtf16LeWithBom {
final byteData = ByteData(length * 2);
final bom = [0xFF, 0xFE];
for (int i = 0; i < length; i++) {
int charCode = codeUnitAt(i);
byteData.setUint16(i * 2, charCode, Endian.little);
}
return bom + byteData.buffer.asUint8List();
}
Uint8List? get getBase64 {
final regExp = RegExp(r'base64,(.*)');
final match = regExp.firstMatch(this);
final realValue = match?.group(1) ?? '';
if (realValue.isEmpty) {
return null;
}
try {
return base64.decode(realValue);
} catch (e) {
return null;
}
}
bool get isRegex {
try {
RegExp(this);
return true;
} catch (e) {
debugPrint(e.toString());
return false;
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/services.dart';
@@ -24,6 +25,16 @@ class System {
return result.exitCode == 0;
}
Future<int> get version async {
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
return switch (Platform.operatingSystem) {
"macos" => (deviceInfo as MacOsDeviceInfo).majorVersion,
"android" => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
"windows" => (deviceInfo as WindowsDeviceInfo).majorVersion,
String() => 0
};
}
back() async {
await app?.moveTaskToBack();
await window?.hide();

18
lib/common/window.dart Normal file → Executable file
View File

@@ -9,7 +9,7 @@ import 'protocol.dart';
import 'system.dart';
class Window {
init(WindowProps props) async {
init(WindowProps props, int version) async {
if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
protocol.register("clash");
@@ -20,8 +20,6 @@ class Window {
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 500),
windowButtonVisibility: false,
titleBarStyle: TitleBarStyle.hidden,
);
if (props.left != null || props.top != null) {
await windowManager.setPosition(
@@ -30,17 +28,22 @@ class Window {
} else {
await windowManager.setAlignment(Alignment.center);
}
// if(Platform.isWindows){
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
// }
if(!Platform.isMacOS || version > 10){
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
await windowManager.waitUntilReadyToShow(windowOptions, () async {
// await windowManager.setPreventClose(true);
await windowManager.setPreventClose(true);
});
}
show() async {
await windowManager.show();
await windowManager.focus();
await windowManager.setSkipTaskbar(false);
}
Future<bool> isVisible() async {
return await windowManager.isVisible();
}
close() async {
@@ -49,6 +52,7 @@ class Window {
hide() async {
await windowManager.hide();
await windowManager.setSkipTaskbar(true);
}
}

View File

@@ -1,6 +1,8 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:path/path.dart';
class Windows {
static Windows? _instance;
@@ -54,6 +56,62 @@ class Windows {
}
return true;
}
Future<bool> registerTask(String appName) async {
final taskXml = '''
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Triggers>
<LogonTrigger/>
</Triggers>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"${Platform.resolvedExecutable}"</Command>
</Exec>
</Actions>
</Task>''';
final taskPath = join(await appPath.tempPath, "task.xml");
await File(taskPath).create(recursive: true);
await File(taskPath)
.writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
final commandLine = [
'/Create',
'/TN',
appName,
'/XML',
"%s",
'/F',
].join(" ");
return runas(
'schtasks',
commandLine.replaceFirst("%s", taskPath),
);
}
}
final windows = Platform.isWindows ? Windows() : null;

View File

@@ -2,15 +2,17 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:url_launcher/url_launcher.dart';
import 'clash/core.dart';
@@ -20,9 +22,9 @@ import 'common/common.dart';
class AppController {
final BuildContext context;
late AppState appState;
late AppFlowingState appFlowingState;
late Config config;
late ClashConfig clashConfig;
late Measure measure;
late Function updateClashConfigDebounce;
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
@@ -32,6 +34,7 @@ class AppController {
appState = context.read<AppState>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
appFlowingState = context.read<AppFlowingState>();
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
@@ -44,7 +47,6 @@ class AppController {
updateGroupDebounce = debounce(() async {
await updateGroups();
});
measure = Measure.of(context);
}
updateStatus(bool isStart) async {
@@ -59,13 +61,15 @@ class AppController {
updateRunTime,
updateTraffic,
];
applyProfileDebounce();
if (!Platform.isAndroid) {
applyProfileDebounce();
}
} else {
await globalState.handleStop();
clashCore.resetTraffic();
appState.traffics = [];
appState.totalTraffic = Traffic();
appState.runTime = null;
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
addCheckIpNumDebounce();
}
}
@@ -79,15 +83,15 @@ class AppController {
if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appState.runTime = nowTimeStamp - startTimeStamp;
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
} else {
appState.runTime = null;
appFlowingState.runTime = null;
}
}
updateTraffic() {
globalState.updateTraffic(
appState: appState,
appFlowingState: appFlowingState,
);
}
@@ -105,6 +109,7 @@ class AppController {
final updateId = config.profiles.first.id;
changeProfile(updateId);
} else {
changeProfile(null);
updateStatus(false);
}
}
@@ -118,11 +123,15 @@ class AppController {
}
Future<void> updateClashConfig({bool isPatch = true}) async {
await globalState.updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
);
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
);
});
}
Future applyProfile({bool isPrue = false}) async {
@@ -165,7 +174,7 @@ class AppController {
try {
updateProfile(profile);
} catch (e) {
appState.addLog(
appFlowingState.addLog(
Log(
logLevel: LogLevel.info,
payload: e.toString(),
@@ -220,7 +229,7 @@ class AppController {
}
handleBackOrExit() async {
if (config.isMinimizeOnExit) {
if (config.appSetting.minimizeOnExit) {
if (system.isDesktop) {
await savePreferences();
}
@@ -232,22 +241,23 @@ class AppController {
handleExit() async {
await updateStatus(false);
await proxy?.stopProxy();
await savePreferences();
clashCore.shutdown();
system.exit();
}
updateLogStatus() {
if (config.openLogs) {
if (config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
appState.logs = [];
appFlowingState.logs = [];
}
}
autoCheckUpdate() async {
if (!config.autoCheckUpdate) return;
if (!config.appSetting.autoCheckUpdate) return;
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
}
@@ -296,8 +306,12 @@ class AppController {
}
init() async {
final isDisclaimerAccepted = await handlerDisclaimer();
if (!isDisclaimerAccepted) {
handleExit();
}
updateLogStatus();
if (!config.silentLaunch) {
if (!config.appSetting.silentLaunch) {
window?.show();
}
if (Platform.isAndroid) {
@@ -306,7 +320,7 @@ class AppController {
if (globalState.isStart) {
await updateStatus(true);
} else {
await updateStatus(config.autoRun);
await updateStatus(config.appSetting.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
@@ -321,7 +335,7 @@ class AppController {
return;
}
appState.currentLabel = appState.currentNavigationItems[index].label;
if ((config.isAnimateToPage || hasAnimate)) {
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
globalState.pageController?.animateToPage(
index,
duration: kTabScrollDuration,
@@ -374,6 +388,49 @@ class AppController {
globalState.showSnackBar(context, message: message);
}
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
dismissible: false,
child: AlertDialog(
title: Text(appLocalizations.disclaimer),
content: Container(
width: dialogCommonWidth,
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: SelectableText(
appLocalizations.disclaimerDesc,
),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop<bool>(false);
},
child: Text(appLocalizations.exit),
),
TextButton(
onPressed: () {
config.appSetting = config.appSetting.copyWith(
disclaimerAccepted: true,
);
Navigator.of(context).pop<bool>(true);
},
child: Text(appLocalizations.agree),
)
],
),
) ??
false;
}
Future<bool> handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) {
return true;
}
return showDisclaimer();
}
addProfileFormURL(String url) async {
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
@@ -433,8 +490,8 @@ class AppController {
return List.of(proxies)
..sort(
(a, b) => other.sortByChar(
PinyinHelper.getPinyin(a.name),
PinyinHelper.getPinyin(b.name),
other.getPinyin(a.name),
other.getPinyin(b.name),
),
);
}
@@ -460,7 +517,7 @@ class AppController {
}
List<Proxy> getSortProxies(List<Proxy> proxies) {
return switch (config.proxiesSortType) {
return switch (config.proxiesStyle.sortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(proxies),
ProxiesSortType.name => _sortOfName(proxies),
@@ -474,6 +531,67 @@ class AppController {
'';
}
updateTun() {
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
);
}
updateSystemProxy() {
config.desktopProps = config.desktopProps.copyWith(
systemProxy: !config.desktopProps.systemProxy,
);
}
updateStart() {
updateStatus(!appFlowingState.isStart);
}
updateAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
autoLaunch: !config.appSetting.autoLaunch,
);
}
updateAdminAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: !config.appSetting.adminAutoLaunch,
);
}
updateVisible() async {
final visible = await window?.isVisible();
if (visible != null && !visible) {
window?.show();
} else {
window?.hide();
}
}
updateMode() {
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
if (index == -1) {
return;
}
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
clashConfig.mode = Mode.values[nextIndex];
}
Future<bool> exportLogs() async {
final logsRaw = appFlowingState.logs.map(
(item) => item.toString(),
);
final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString);
});
return await picker.saveFile(
other.logFile,
Uint8List.fromList(data),
) !=
null;
}
Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath();
final profilesPath = await appPath.getProfilesPath();
@@ -489,6 +607,124 @@ class AppController {
});
}
Future _updateSystemTray({
required bool isStart,
required Brightness? brightness,
bool force = false,
}) async {
if (Platform.isLinux || force) {
await trayManager.destroy();
}
await trayManager.setIcon(
other.getTrayIconPath(
isStart: isStart,
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
isTemplate: true,
);
if (!Platform.isLinux) {
await trayManager.setToolTip(
appName,
);
}
}
updateTray([bool focus = false]) async {
if (!Platform.isLinux) {
await _updateSystemTray(
isStart: appFlowingState.isStart,
brightness: appState.brightness,
force: focus,
);
}
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
onClick: (_) {
window?.show();
},
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
checked: false,
);
menuItems.add(startMenuItem);
menuItems.add(MenuItem.separator());
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == clashConfig.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (appFlowingState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: clashConfig.tun.enable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: config.desktopProps.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: config.appSetting.autoLaunch,
);
final adminAutoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.adminAutoLaunch,
onClick: (_) async {
globalState.appController.updateAdminAutoLaunch();
},
checked: config.appSetting.adminAutoLaunch,
);
menuItems.add(autoStartMenuItem);
menuItems.add(adminAutoStartMenuItem);
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
onClick: (_) async {
await globalState.appController.handleExit();
},
);
menuItems.add(exitMenuItem);
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
isStart: appFlowingState.isStart,
brightness: appState.brightness,
force: focus,
);
}
}
recoveryData(
List<int> data,
RecoveryOption recoveryOption,

View File

@@ -1,5 +1,9 @@
// ignore_for_file: constant_identifier_names
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -11,6 +15,10 @@ extension GroupTypeExtension on GroupType {
)
.toList();
bool get isURLTestOrFallback {
return [GroupType.URLTest, GroupType.Fallback].contains(this);
}
static GroupType? getGroupType(String value) {
final index = GroupTypeExtension.valueList.indexOf(value);
if (index == -1) return null;
@@ -86,6 +94,82 @@ enum CommonCardType { plain, filled }
enum ProxiesType { tab, list }
enum ProxiesLayout{ loose, standard, tight }
enum ProxiesLayout { loose, standard, tight }
enum ProxyCardType { expand, shrink, min }
enum DnsMode {
normal,
@JsonValue("fake-ip")
fakeIp,
@JsonValue("redir-host")
redirHost,
hosts
}
enum KeyboardModifier {
alt([
PhysicalKeyboardKey.altLeft,
PhysicalKeyboardKey.altRight,
]),
capsLock([
PhysicalKeyboardKey.capsLock,
]),
control([
PhysicalKeyboardKey.controlLeft,
PhysicalKeyboardKey.controlRight,
]),
fn([
PhysicalKeyboardKey.fn,
]),
meta([
PhysicalKeyboardKey.metaLeft,
PhysicalKeyboardKey.metaRight,
]),
shift([
PhysicalKeyboardKey.shiftLeft,
PhysicalKeyboardKey.shiftRight,
]);
final List<PhysicalKeyboardKey> physicalKeys;
const KeyboardModifier(this.physicalKeys);
}
extension KeyboardModifierExt on KeyboardModifier {
HotKeyModifier toHotKeyModifier() {
return switch (this) {
KeyboardModifier.alt => HotKeyModifier.alt,
KeyboardModifier.capsLock => HotKeyModifier.capsLock,
KeyboardModifier.control => HotKeyModifier.control,
KeyboardModifier.fn => HotKeyModifier.fn,
KeyboardModifier.meta => HotKeyModifier.meta,
KeyboardModifier.shift => HotKeyModifier.shift,
};
}
}
enum HotAction {
start,
view,
mode,
proxy,
tun,
}
enum ProxiesIconStyle {
standard,
none,
icon,
}
enum FontFamily {
system(),
miSans("MiSans"),
twEmoji("Twemoji"),
icon("Icons");
final String? value;
const FontFamily([this.value]);
}

View File

@@ -47,7 +47,7 @@ class AboutFragment extends StatelessWidget {
title: const Text("Telegram"),
onTap: () {
globalState.openUrl(
"https://t.me/+G-veVtwBOl4wODc1",
"https://t.me/FlClash",
);
},
trailing: const Icon(Icons.launch),

View File

@@ -8,6 +8,84 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class CloseConnectionsSwitch extends StatelessWidget {
const CloseConnectionsSwitch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.closeConnections,
builder: (_, closeConnections, __) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: closeConnections,
onChanged: (value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
closeConnections: value,
);
},
),
);
},
);
}
}
class UsageSwitch extends StatelessWidget {
const UsageSwitch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.onlyProxy,
builder: (_, onlyProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate(
value: onlyProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
onlyProxy: value,
);
},
),
);
},
);
}
}
class AdminAutoLaunchItem extends StatelessWidget {
const AdminAutoLaunchItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.adminAutoLaunch,
builder: (_, adminAutoLaunch, __) {
return ListItem.switchItem(
title: Text(appLocalizations.adminAutoLaunch),
subtitle: Text(appLocalizations.adminAutoLaunchDesc),
delegate: SwitchDelegate(
value: adminAutoLaunch,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: value,
);
},
),
);
},
);
}
}
class ApplicationSettingFragment extends StatelessWidget {
const ApplicationSettingFragment({super.key});
@@ -20,17 +98,18 @@ class ApplicationSettingFragment extends StatelessWidget {
Widget build(BuildContext context) {
List<Widget> items = [
Selector<Config, bool>(
selector: (_, config) => config.isMinimizeOnExit,
selector: (_, config) => config.appSetting.minimizeOnExit,
builder: (_, isMinimizeOnExit, child) {
return ListItem.switchItem(
leading: const Icon(Icons.back_hand),
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: isMinimizeOnExit,
onChanged: (bool value) {
final config = context.read<Config>();
config.isMinimizeOnExit = value;
config.appSetting = config.appSetting.copyWith(
minimizeOnExit: value,
);
},
),
);
@@ -38,52 +117,57 @@ class ApplicationSettingFragment extends StatelessWidget {
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.autoLaunch,
selector: (_, config) => config.appSetting.autoLaunch,
builder: (_, autoLaunch, child) {
return ListItem.switchItem(
leading: const Icon(Icons.rocket_launch),
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
final config = context.read<Config>();
config.autoLaunch = value;
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoLaunch: value,
);
},
),
);
},
),
if(Platform.isWindows)
const AdminAutoLaunchItem(),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.silentLaunch,
selector: (_, config) => config.appSetting.silentLaunch,
builder: (_, silentLaunch, child) {
return ListItem.switchItem(
leading: const Icon(Icons.expand_circle_down),
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
final config = context.read<Config>();
config.silentLaunch = value;
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
silentLaunch: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.autoRun,
selector: (_, config) => config.appSetting.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
leading: const Icon(Icons.not_started),
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
final config = context.read<Config>();
config.autoRun = value;
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoRun: value,
);
},
),
);
@@ -91,17 +175,18 @@ class ApplicationSettingFragment extends StatelessWidget {
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.isExclude,
selector: (_, config) => config.appSetting.hidden,
builder: (_, isExclude, child) {
return ListItem.switchItem(
leading: const Icon(Icons.visibility_off),
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: isExclude,
onChanged: (value) {
final config = context.read<Config>();
config.isExclude = value;
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
hidden: value,
);
},
),
);
@@ -109,52 +194,56 @@ class ApplicationSettingFragment extends StatelessWidget {
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.isAnimateToPage,
selector: (_, config) => config.appSetting.isAnimateToPage,
builder: (_, isAnimateToPage, child) {
return ListItem.switchItem(
leading: const Icon(Icons.animation),
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
final config = context.read<Config>();
config.isAnimateToPage = value;
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
isAnimateToPage: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.openLogs,
selector: (_, config) => config.appSetting.openLogs,
builder: (_, openLogs, child) {
return ListItem.switchItem(
leading: const Icon(Icons.bug_report),
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
final config = context.read<Config>();
config.openLogs = value;
globalState.appController.updateLogStatus();
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
openLogs: value,
);
},
),
);
},
),
const CloseConnectionsSwitch(),
const UsageSwitch(),
Selector<Config, bool>(
selector: (_, config) => config.autoCheckUpdate,
selector: (_, config) => config.appSetting.autoCheckUpdate,
builder: (_, autoCheckUpdate, child) {
return ListItem.switchItem(
leading: const Icon(Icons.system_update),
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
final config = context.read<Config>();
config.autoCheckUpdate = value;
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoCheckUpdate: value,
);
},
),
);

View File

@@ -3,8 +3,7 @@ import 'dart:typed_data';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/config.dart';
import 'package:fl_clash/models/dav.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart';
@@ -73,11 +72,11 @@ class BackupAndRecovery extends StatelessWidget {
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
final backupData = await globalState.appController.backupData();
final value = await picker.saveFile(
final value = await picker.saveFile(
other.getBackupFileName(),
Uint8List.fromList(backupData),
);
if(value == null) return false;
if (value == null) return false;
return true;
},
title: appLocalizations.backup,
@@ -204,6 +203,24 @@ class BackupAndRecovery extends StatelessWidget {
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.appController.config.dav =
globalState.appController.config.dav?.copyWith(
fileName: value,
);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);

View File

@@ -1,689 +0,0 @@
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:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ConfigFragment extends StatefulWidget {
const ConfigFragment({super.key});
@override
State<ConfigFragment> createState() => _ConfigFragmentState();
}
class _ConfigFragmentState extends State<ConfigFragment> {
_modifyMixedPort(num mixedPort) async {
final port = await globalState.showCommonDialog(
child: MixedPortFormDialog(
mixedPort: mixedPort,
),
);
if (port != null && port != mixedPort && mounted) {
try {
final mixedPort = int.parse(port);
if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port";
globalState.appController.clashConfig.mixedPort = mixedPort;
} catch (e) {
globalState.showMessage(
title: appLocalizations.proxyPort,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
_showLogLevelDialog(LogLevel value) {
globalState.showCommonDialog(
child: AlertDialog(
title: Text(appLocalizations.logLevel),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
for (final logLevel in LogLevel.values)
ListItem.radio(
delegate: RadioDelegate<LogLevel>(
value: logLevel,
groupValue: value,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
Navigator.of(context).pop();
},
),
title: Text(logLevel.name),
)
],
),
),
),
);
}
_showUaDialog(String? value) {
const uas = [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
];
globalState.showCommonDialog(
child: AlertDialog(
title: const Text("UA"),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
content: SizedBox(
width: 250,
child: Wrap(
children: [
for (final ua in uas)
ListItem.radio(
delegate: RadioDelegate<String?>(
value: ua,
groupValue: value,
onChanged: (String? value) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = value;
Navigator.of(context).pop();
},
),
title: Text(ua ?? appLocalizations.defaultText),
)
],
),
),
),
);
}
_modifyTestUrl(String testUrl) async {
final newTestUrl = await globalState.showCommonDialog<String>(
child: TestUrlFormDialog(
testUrl: testUrl,
),
);
if (newTestUrl != null && newTestUrl != testUrl && mounted) {
try {
if (!newTestUrl.isUrl) {
throw "Invalid url";
}
globalState.appController.config.testUrl = newTestUrl;
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
_updateKeepAliveInterval(int keepAliveInterval) async {
final newKeepAliveIntervalString =
await globalState.showCommonDialog<String>(
child: KeepAliveIntervalFormDialog(
keepAliveInterval: keepAliveInterval,
),
);
if (newKeepAliveIntervalString != null &&
newKeepAliveIntervalString != "$keepAliveInterval" &&
mounted) {
try {
final newKeepAliveInterval = int.parse(newKeepAliveIntervalString);
if (newKeepAliveInterval <= 0) {
throw "Invalid keepAliveInterval";
}
globalState.appController.clashConfig.keepAliveInterval =
newKeepAliveInterval;
globalState.appController.updateClashConfigDebounce();
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
}
}
List<Widget> _buildAppSection() {
return generateSection(
title: appLocalizations.app,
items: [
if (Platform.isAndroid) ...[
Selector<Config, bool>(
selector: (_, config) => config.vpnProps.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
leading: const Icon(Icons.arrow_forward_outlined),
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
allowBypass: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.vpnProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.settings_ethernet),
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
systemProxy: value,
);
},
),
);
},
),
],
Selector<Config, bool>(
selector: (_, config) => config.isCloseConnections,
builder: (_, isCloseConnections, __) {
return ListItem.switchItem(
leading: const Icon(Icons.auto_delete_outlined),
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: isCloseConnections,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCloseConnections = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.onlyProxy,
builder: (_, onlyProxy, __) {
return ListItem.switchItem(
leading: const Icon(Icons.data_usage_outlined),
title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate(
value: onlyProxy,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.onlyProxy = value;
},
),
);
},
),
// Selector<Config, bool>(
// selector: (_, config) => config.isCompatible,
// builder: (_, isCompatible, __) {
// return ListItem.switchItem(
// leading: const Icon(Icons.expand_outlined),
// title: Text(appLocalizations.compatible),
// subtitle: Text(appLocalizations.compatibleDesc),
// delegate: SwitchDelegate(
// value: isCompatible,
// onChanged: (bool value) async {
// final appController = globalState.appController;
// appController.config.isCompatible = value;
// await appController.applyProfile();
// },
// ),
// );
// },
// ),
],
);
}
List<Widget> _buildGeneralSection() {
return generateSection(
title: appLocalizations.general,
items: [
Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTap: () {
_showLogLevelDialog(value);
},
);
},
),
Selector<ClashConfig, String?>(
selector: (_, clashConfig) => clashConfig.globalRealUa,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
onTap: () {
_showUaDialog(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, config) => config.keepAliveInterval,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$value ${appLocalizations.seconds}"),
onTap: () {
_updateKeepAliveInterval(value);
},
);
},
),
Selector<Config, String>(
selector: (_, config) => config.testUrl,
builder: (_, value, __) {
return ListItem(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
onTap: () {
_modifyTestUrl(value);
},
);
},
),
Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTap: () {
_modifyMixedPort(mixedPort);
},
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text(appLocalizations.proxyPortDesc),
trailing: FilledButton.tonal(
onPressed: () {
_modifyMixedPort(mixedPort);
},
child: Text(
"$mixedPort",
),
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader = value
? geodataLoaderMemconservative
: geodataLoaderStandard;
},
),
);
},
),
Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
},
),
);
},
),
],
);
}
List<Widget> _buildMoreSection() {
return generateSection(
title: appLocalizations.more,
items: [
if (system.isDesktop)
Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
builder: (_, tunEnable, __) {
return ListItem.switchItem(
leading: const Icon(Icons.important_devices_outlined),
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: tunEnable,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.tun = Tun(enable: value);
},
),
);
},
),
],
);
}
@override
Widget build(BuildContext context) {
List<Widget> items = [
..._buildAppSection(),
..._buildGeneralSection(),
..._buildMoreSection(),
];
return ListView.builder(
padding: const EdgeInsets.only(bottom: 32),
itemBuilder: (_, index) {
return Container(
alignment: Alignment.center,
child: items[index],
);
},
itemCount: items.length,
);
}
}
class MixedPortFormDialog extends StatefulWidget {
final num mixedPort;
const MixedPortFormDialog({super.key, required this.mixedPort});
@override
State<MixedPortFormDialog> createState() => _MixedPortFormDialogState();
}
class _MixedPortFormDialogState extends State<MixedPortFormDialog> {
late TextEditingController portController;
@override
void initState() {
super.initState();
portController = TextEditingController(text: "${widget.mixedPort}");
}
_handleUpdate() async {
final port = portController.value.text;
if (port.isEmpty) return;
Navigator.of(context).pop<String>(port);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.proxyPort),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
controller: portController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}
class TestUrlFormDialog extends StatefulWidget {
final String testUrl;
const TestUrlFormDialog({
super.key,
required this.testUrl,
});
@override
State<TestUrlFormDialog> createState() => _TestUrlFormDialogState();
}
class _TestUrlFormDialogState extends State<TestUrlFormDialog> {
late TextEditingController testUrlController;
@override
void initState() {
super.initState();
testUrlController = TextEditingController(text: widget.testUrl);
}
_handleUpdate() async {
final testUrl = testUrlController.value.text;
if (testUrl.isEmpty) return;
Navigator.of(context).pop<String>(testUrl);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.testUrl),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
maxLines: 5,
minLines: 1,
controller: testUrlController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}
class KeepAliveIntervalFormDialog extends StatefulWidget {
final int keepAliveInterval;
const KeepAliveIntervalFormDialog({
super.key,
required this.keepAliveInterval,
});
@override
State<KeepAliveIntervalFormDialog> createState() =>
_KeepAliveIntervalFormDialogState();
}
class _KeepAliveIntervalFormDialogState
extends State<KeepAliveIntervalFormDialog> {
late TextEditingController keepAliveIntervalController;
@override
void initState() {
super.initState();
keepAliveIntervalController = TextEditingController(
text: "${widget.keepAliveInterval}",
);
}
_handleUpdate() async {
final keepAliveInterval = keepAliveIntervalController.value.text;
if (keepAliveInterval.isEmpty) return;
Navigator.of(context).pop<String>(keepAliveInterval);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appLocalizations.keepAliveIntervalDesc),
content: SizedBox(
width: 300,
child: Wrap(
runSpacing: 16,
children: [
TextField(
maxLines: 1,
minLines: 1,
controller: keepAliveIntervalController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
suffixText: appLocalizations.seconds,
),
),
],
),
),
actions: [
TextButton(
onPressed: _handleUpdate,
child: Text(appLocalizations.submit),
)
],
);
}
}

View File

@@ -0,0 +1,66 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/dns.dart';
import 'package:fl_clash/fragments/config/general.dart';
import 'package:fl_clash/fragments/config/network.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class ConfigFragment extends StatefulWidget {
const ConfigFragment({super.key});
@override
State<ConfigFragment> createState() => _ConfigFragmentState();
}
class _ConfigFragmentState extends State<ConfigFragment> {
@override
Widget build(BuildContext context) {
List<Widget> items = [
ListItem.open(
title: Text(appLocalizations.network),
subtitle: Text(appLocalizations.networkDesc),
leading: const Icon(Icons.vpn_key),
delegate: OpenDelegate(
title: appLocalizations.network,
isScaffold: true,
isBlur: false,
widget: const NetworkListView(),
),
),
ListItem.open(
title: Text(appLocalizations.general),
subtitle: Text(appLocalizations.generalDesc),
leading: const Icon(Icons.build),
delegate: OpenDelegate(
title: appLocalizations.general,
widget: generateListView(
generalItems,
),
isBlur: false,
extendPageWidth: 360,
),
),
ListItem.open(
title: const Text("DNS"),
subtitle: Text(appLocalizations.dnsDesc),
leading: const Icon(Icons.dns),
delegate: const OpenDelegate(
title: "DNS",
widget: DnsListView(),
isScaffold: true,
isBlur: false,
extendPageWidth: 360,
),
)
];
return generateListView(
items
.separated(
const Divider(
height: 0,
),
)
.toList(),
);
}
}

View File

@@ -0,0 +1,739 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class OverrideItem extends StatelessWidget {
const OverrideItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.overrideDns,
builder: (_, override, __) {
return ListItem.switchItem(
title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate(
value: override,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.overrideDns = value;
},
),
);
},
);
}
}
class StatusItem extends StatelessWidget {
const StatusItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.status),
subtitle: Text(appLocalizations.statusDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
enable: value,
);
},
),
);
},
);
}
}
class PreferH3Item extends StatelessWidget {
const PreferH3Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.preferH3,
builder: (_, preferH3, __) {
return ListItem.switchItem(
title: const Text("PreferH3"),
subtitle: Text(appLocalizations.preferH3Desc),
delegate: SwitchDelegate(
value: preferH3,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
preferH3: value,
);
},
),
);
},
);
}
}
class IPv6Item extends StatelessWidget {
const IPv6Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
ipv6: value,
);
},
),
);
},
);
}
}
class RespectRulesItem extends StatelessWidget {
const RespectRulesItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.respectRules,
builder: (_, respectRules, __) {
return ListItem.switchItem(
title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate(
value: respectRules,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
respectRules: value,
);
},
),
);
},
);
}
}
class DnsModeItem extends StatelessWidget {
const DnsModeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, DnsMode>(
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
builder: (_, enhancedMode, __) {
return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate(
title: appLocalizations.dnsMode,
options: DnsMode.values,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(enhancedMode: value);
},
textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode,
),
);
},
);
}
}
class FakeIpRangeItem extends StatelessWidget {
const FakeIpRangeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
builder: (_, fakeIpRange, __) {
return ListItem.input(
title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange),
delegate: InputDelegate(
title: appLocalizations.fakeipRange,
value: fakeIpRange,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
clashConfig.dns = clashConfig.dns.copyWith(
fakeIpRange: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
class FakeIpFilterItem extends StatelessWidget {
const FakeIpFilterItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.fakeipFilter),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.fakeipFilter,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fakeIpFilter, __) {
return ListPage(
title: appLocalizations.fakeipFilter,
items: fakeIpFilter,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fakeIpFilter: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class DefaultNameserverItem extends StatelessWidget {
const DefaultNameserverItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.defaultNameserver),
subtitle: Text(appLocalizations.defaultNameserverDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.defaultNameserver,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, defaultNameserver, __) {
return ListPage(
title: appLocalizations.defaultNameserver,
items: defaultNameserver,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
defaultNameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class NameserverItem extends StatelessWidget {
const NameserverItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.nameserver),
subtitle: Text(appLocalizations.nameserverDesc),
delegate: OpenDelegate(
title: appLocalizations.nameserver,
isBlur: false,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, nameserver, __) {
return ListPage(
title: "域名服务器",
items: nameserver,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class UseHostsItem extends StatelessWidget {
const UseHostsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useHosts,
builder: (_, useHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate(
value: useHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useHosts: value,
);
},
),
);
},
);
}
}
class UseSystemHostsItem extends StatelessWidget {
const UseSystemHostsItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
builder: (_, useSystemHosts, __) {
return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate(
value: useSystemHosts,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
useSystemHosts: value,
);
},
),
);
},
);
}
}
class NameserverPolicyItem extends StatelessWidget {
const NameserverPolicyItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.nameserverPolicy),
subtitle: Text(appLocalizations.nameserverPolicyDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.nameserverPolicy,
widget: Selector<ClashConfig, Map<String, String>>(
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, nameserverPolicy, __) {
return ListPage(
title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
nameserverPolicy: Map.fromEntries(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class ProxyServerNameserverItem extends StatelessWidget {
const ProxyServerNameserverItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.proxyNameserver),
subtitle: Text(appLocalizations.proxyNameserverDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.proxyNameserver,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, proxyServerNameserver, __) {
return ListPage(
title: appLocalizations.proxyNameserver,
items: proxyServerNameserver,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
proxyServerNameserver: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class FallbackItem extends StatelessWidget {
const FallbackItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.fallback),
subtitle: Text(appLocalizations.fallbackDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.fallback,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallback,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fallback, __) {
return ListPage(
title: appLocalizations.fallback,
items: fallback,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallback: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class GeoipItem extends StatelessWidget {
const GeoipItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
builder: (_, geoip, __) {
return ListItem.switchItem(
title: const Text("Geoip"),
delegate: SwitchDelegate(
value: geoip,
onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
);
},
),
);
},
);
}
}
class GeoipCodeItem extends StatelessWidget {
const GeoipCodeItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
builder: (_, geoipCode, __) {
return ListItem.input(
title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode),
delegate: InputDelegate(
title: appLocalizations.geoipCode,
value: geoipCode,
onChanged: (String? value) {
if (value != null) {
try {
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geoipCode: value,
),
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
class GeositeItem extends StatelessWidget {
const GeositeItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: const Text("Geosite"),
delegate: OpenDelegate(
isBlur: false,
title: "Geosite",
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, geosite, __) {
return ListPage(
title: "Geosite",
items: geosite,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
geosite: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class IpcidrItem extends StatelessWidget {
const IpcidrItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.ipcidr),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.ipcidr,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, ipcidr, __) {
return ListPage(
title: appLocalizations.ipcidr,
items: ipcidr,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
ipcidr: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class DomainItem extends StatelessWidget {
const DomainItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.domain),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.domain,
widget: Selector<ClashConfig, List<String>>(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, domain, __) {
return ListPage(
title: appLocalizations.domain,
items: domain,
titleBuilder: (item) => Text(item),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
final dns = clashConfig.dns;
clashConfig.dns = dns.copyWith(
fallbackFilter: dns.fallbackFilter.copyWith(
domain: List.from(items),
),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class DnsOptions extends StatelessWidget {
const DnsOptions({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: generateSection(
title: appLocalizations.options,
items: [
const StatusItem(),
const UseHostsItem(),
const UseSystemHostsItem(),
const IPv6Item(),
const RespectRulesItem(),
const PreferH3Item(),
const DnsModeItem(),
const FakeIpRangeItem(),
const FakeIpFilterItem(),
const DefaultNameserverItem(),
const NameserverPolicyItem(),
const NameserverItem(),
const FallbackItem(),
const ProxyServerNameserverItem(),
],
),
);
}
}
class FallbackFilterOptions extends StatelessWidget {
const FallbackFilterOptions({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: generateSection(
title: appLocalizations.fallbackFilter,
items: [
const GeoipItem(),
const GeoipCodeItem(),
const GeositeItem(),
const IpcidrItem(),
const DomainItem(),
],
),
);
}
}
const dnsItems = <Widget>[
OverrideItem(),
DnsOptions(),
FallbackFilterOptions(),
];
class DnsListView extends StatelessWidget {
const DnsListView({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
onTab: () {
globalState.appController.clashConfig.dns = defaultDns;
Navigator.of(context).pop();
});
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
_initActions(context);
return generateListView(
dnsItems,
);
}
}

View File

@@ -0,0 +1,436 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class LogLevelItem extends StatelessWidget {
const LogLevelItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, LogLevel>(
selector: (_, clashConfig) => clashConfig.logLevel,
builder: (_, value, __) {
return ListItem<LogLevel>.options(
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
delegate: OptionsDelegate<LogLevel>(
title: appLocalizations.logLevel,
options: LogLevel.values,
onChanged: (LogLevel? value) {
if (value == null) {
return;
}
final appController = globalState.appController;
appController.clashConfig.logLevel = value;
},
textBuilder: (logLevel) => logLevel.name,
value: value,
),
);
},
);
}
}
class UaItem extends StatelessWidget {
const UaItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, String?>(
selector: (_, clashConfig) => clashConfig.globalRealUa,
builder: (_, value, __) {
return ListItem<String?>.options(
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>(
title: "UA",
options: [
null,
"clash-verge/v1.6.6",
"ClashforWindows/0.19.23",
],
value: value,
onChanged: (ua) {
final appController = globalState.appController;
appController.clashConfig.globalRealUa = ua;
},
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
),
);
},
);
}
}
class KeepAliveIntervalItem extends StatelessWidget {
const KeepAliveIntervalItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, int>(
selector: (_, config) => config.keepAliveInterval,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.timer_outlined),
title: Text(appLocalizations.keepAliveIntervalDesc),
subtitle: Text("$value ${appLocalizations.seconds}"),
delegate: InputDelegate(
title: appLocalizations.keepAliveIntervalDesc,
suffixText: appLocalizations.seconds,
resetValue: "$defaultKeepAliveInterval",
value: "$value",
onChanged: (String? value) {
if (value != null) {
try {
final intValue = int.parse(value);
if (intValue <= 0) {
throw "Invalid keepAliveInterval";
}
globalState.appController.clashConfig.keepAliveInterval =
intValue;
} catch (e) {
globalState.showMessage(
title: appLocalizations.keepAliveIntervalDesc,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
class TestUrlItem extends StatelessWidget {
const TestUrlItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, String>(
selector: (_, config) => config.appSetting.testUrl,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
delegate: InputDelegate(
resetValue: defaultTestUrl,
title: appLocalizations.testUrl,
value: value,
onChanged: (String? value) {
if (value != null) {
try {
if (!value.isUrl) {
throw "Invalid url";
}
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
testUrl: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
);
}
}
class MixedPortItem extends StatelessWidget {
const MixedPortItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, int>(
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, value, __) {
return ListItem.input(
leading: const Icon(Icons.adjust_outlined),
title: Text(appLocalizations.proxyPort),
subtitle: Text("$value"),
delegate: InputDelegate(
title: appLocalizations.proxyPort,
value: "$value",
onChanged: (String? value) {
if (value != null) {
try {
final mixedPort = int.parse(value);
if (mixedPort < 1024 || mixedPort > 49151) {
throw "Invalid port";
}
globalState.appController.clashConfig.mixedPort = mixedPort;
} catch (e) {
globalState.showMessage(
title: appLocalizations.proxyPort,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
resetValue: "$defaultMixedPort",
),
);
},
);
}
}
class HostsItem extends StatelessWidget {
const HostsItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.view_list_outlined),
title: const Text("Hosts"),
subtitle: Text(appLocalizations.hostsDesc),
delegate: OpenDelegate(
isBlur: false,
title: "Hosts",
widget: Selector<ClashConfig, HostsMap>(
selector: (_, clashConfig) => clashConfig.hosts,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, hosts, ___) {
final entries = hosts.entries;
return ListPage(
title: "Hosts",
items: entries,
titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value),
onChange: (items){
final clashConfig = globalState.appController.clashConfig;
clashConfig.hosts = Map.fromEntries(items);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
class Ipv6Item extends StatelessWidget {
const Ipv6Item({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
leading: const Icon(Icons.water_outlined),
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6Desc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.ipv6 = value;
},
),
);
},
);
}
}
class AllowLanItem extends StatelessWidget {
const AllowLanItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.allowLan,
builder: (_, allowLan, __) {
return ListItem.switchItem(
leading: const Icon(Icons.device_hub),
title: Text(appLocalizations.allowLan),
subtitle: Text(appLocalizations.allowLanDesc),
delegate: SwitchDelegate(
value: allowLan,
onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>();
clashConfig.allowLan = value;
},
),
);
},
);
}
}
class UnifiedDelayItem extends StatelessWidget {
const UnifiedDelayItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.unifiedDelay,
builder: (_, unifiedDelay, __) {
return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate(
value: unifiedDelay,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.unifiedDelay = value;
},
),
);
},
);
}
}
class FindProcessItem extends StatelessWidget {
const FindProcessItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) {
return ListItem.switchItem(
leading: const Icon(Icons.polymer_outlined),
title: Text(appLocalizations.findProcessMode),
subtitle: Text(appLocalizations.findProcessModeDesc),
delegate: SwitchDelegate(
value: findProcess,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.findProcessMode =
value ? FindProcessMode.always : FindProcessMode.off;
},
),
);
},
);
}
}
class TcpConcurrentItem extends StatelessWidget {
const TcpConcurrentItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
builder: (_, tcpConcurrent, __) {
return ListItem.switchItem(
leading: const Icon(Icons.double_arrow_outlined),
title: Text(appLocalizations.tcpConcurrent),
subtitle: Text(appLocalizations.tcpConcurrentDesc),
delegate: SwitchDelegate(
value: tcpConcurrent,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.tcpConcurrent = value;
},
),
);
},
);
}
}
class GeodataLoaderItem extends StatelessWidget {
const GeodataLoaderItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) =>
clashConfig.geodataLoader == geodataLoaderMemconservative,
builder: (_, memconservative, __) {
return ListItem.switchItem(
leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate(
value: memconservative,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.geodataLoader =
value ? geodataLoaderMemconservative : geodataLoaderStandard;
},
),
);
},
);
}
}
class ExternalControllerItem extends StatelessWidget {
const ExternalControllerItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
builder: (_, hasExternalController, __) {
return ListItem.switchItem(
leading: const Icon(Icons.api_outlined),
title: Text(appLocalizations.externalController),
subtitle: Text(appLocalizations.externalControllerDesc),
delegate: SwitchDelegate(
value: hasExternalController,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.clashConfig.externalController =
value ? defaultExternalController : '';
},
),
);
},
);
}
}
final generalItems = const [
LogLevelItem(),
UaItem(),
KeepAliveIntervalItem(),
TestUrlItem(),
MixedPortItem(),
HostsItem(),
Ipv6Item(),
AllowLanItem(),
UnifiedDelayItem(),
FindProcessItem(),
TcpConcurrentItem(),
GeodataLoaderItem(),
ExternalControllerItem(),
]
.separated(
const Divider(
height: 0,
),
)
.toList();

View File

@@ -0,0 +1,270 @@
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:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class VPNSwitch extends StatelessWidget {
const VPNSwitch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: const Text("VPN"),
subtitle: Text(appLocalizations.vpnEnableDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
final config = globalState.appController.config;
config.vpnProps = config.vpnProps.copyWith(
enable: value,
);
},
),
);
},
);
}
}
class TUNItem extends StatelessWidget {
const TUNItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate(
value: enable,
onChanged: (value) async {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
),
);
},
);
}
}
class AllowBypassSwitch extends StatelessWidget {
const AllowBypassSwitch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.allowBypass,
builder: (_, allowBypass, __) {
return ListItem.switchItem(
title: Text(appLocalizations.allowBypass),
subtitle: Text(appLocalizations.allowBypassDesc),
delegate: SwitchDelegate(
value: allowBypass,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
allowBypass: value,
);
},
),
);
},
);
}
}
class SystemProxySwitch extends StatelessWidget {
const SystemProxySwitch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
systemProxy: value,
);
},
),
);
},
);
}
}
class Ipv6Switch extends StatelessWidget {
const Ipv6Switch({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.ipv6,
builder: (_, ipv6, __) {
return ListItem.switchItem(
title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate(
value: ipv6,
onChanged: (bool value) async {
final config = globalState.appController.config;
final vpnProps = config.vpnProps;
config.vpnProps = vpnProps.copyWith(
ipv6: value,
);
},
),
);
},
);
}
}
class TunStackItem extends StatelessWidget {
const TunStackItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, TunStack>(
selector: (_, clashConfig) => clashConfig.tun.stack,
builder: (_, stack, __) {
return ListItem.options(
title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>(
value: stack,
options: TunStack.values,
textBuilder: (value) => value.name,
onChanged: (value) {
if (value == null) {
return;
}
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
stack: value,
);
},
title: appLocalizations.stackMode,
),
);
},
);
}
}
class BypassDomainItem extends StatelessWidget {
const BypassDomainItem({super.key});
@override
Widget build(BuildContext context) {
return ListItem.open(
title: Text(appLocalizations.bypassDomain),
subtitle: Text(appLocalizations.bypassDomainDesc),
delegate: OpenDelegate(
isBlur: false,
title: appLocalizations.bypassDomain,
widget: Selector<Config, List<String>>(
selector: (_, config) => config.vpnProps.bypassDomain,
shouldRebuild: (prev, next) =>
!stringListEquality.equals(prev, next),
builder: (_, bypassDomain, __) {
return ListPage(
title: appLocalizations.bypassDomain,
items: bypassDomain,
titleBuilder: (item) => Text(item),
onChange: (items){
final config = globalState.appController.config;
config.vpnProps = config.vpnProps.copyWith(
bypassDomain: List.from(items),
);
},
);
},
),
extendPageWidth: 360,
),
);
}
}
final networkItems = [
Platform.isAndroid ? const VPNSwitch() : const TUNItem(),
if (Platform.isAndroid)
...generateSection(
title: "VPN",
items: [
const SystemProxySwitch(),
const AllowBypassSwitch(),
const Ipv6Switch(),
const BypassDomainItem(),
],
),
...generateSection(
title: appLocalizations.options,
items: [
const TunStackItem(),
],
),
];
class NetworkListView extends StatelessWidget {
const NetworkListView({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
onTab: () {
final appController = globalState.appController;
appController.config.vpnProps = defaultVpnProps;
appController.clashConfig.tun = defaultTun;
Navigator.of(context).pop();
},
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
_initActions(context);
return generateListView(
networkItems,
);
}
}

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
@@ -48,11 +47,11 @@ class _DashboardFragmentState extends State<DashboardFragment> {
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
if (Platform.isAndroid)
GridItem(
crossAxisCellCount: switchCount,
child: const VPNSwitch(),
),
// if (Platform.isAndroid)
// GridItem(
// crossAxisCellCount: switchCount,
// child: const VPNSwitch(),
// ),
if (system.isDesktop) ...[
GridItem(
crossAxisCellCount: switchCount,

View File

@@ -13,16 +13,55 @@ class IntranetIP extends StatefulWidget {
}
class _IntranetIPState extends State<IntranetIP> {
final ipNotifier = ValueNotifier<String>("");
final ipNotifier = ValueNotifier<String?>("");
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list();
for (final interface in interfaces) {
for (final address in interface.addresses) {
if (!address.isLoopback) {
return address.address;
Future<String> getNetworkType() async {
try {
List<NetworkInterface> 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 {
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;
}
@@ -48,17 +87,15 @@ class _IntranetIPState extends State<IntranetIP> {
label: appLocalizations.intranetIP,
iconData: Icons.devices,
),
onPressed: (){
},
onPressed: () {},
child: Container(
padding: const EdgeInsets.all(16).copyWith(top: 0),
height: globalState.appController.measure.titleLargeHeight + 24 - 2,
height: globalState.measure.titleMediumHeight + 24 - 2,
child: ValueListenableBuilder(
valueListenable: ipNotifier,
builder: (_, value, __) {
return FadeBox(
child: value.isNotEmpty
child: value != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@@ -67,8 +104,9 @@ class _IntranetIPState extends State<IntranetIP> {
flex: 1,
child: TooltipText(
text: Text(
value,
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
value.isNotEmpty ? value : appLocalizations.noNetwork,
style: context
.textTheme.titleLarge?.toSoftBold.toMinus,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),

View File

@@ -1,5 +1,8 @@
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';
@@ -22,14 +25,17 @@ 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 = appState.isStart;
final isStart = appFlowingState.isStart;
if (_preIsStart == false && _preIsStart == isStart) return;
_clearSetTimeoutTimer();
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
@@ -42,12 +48,31 @@ class _NetworkDetectionState extends State<NetworkDetection> {
cancelToken = CancelToken();
try {
final ipInfo = await request.checkIp(cancelToken: cancelToken);
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
if (ipInfo != null) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: ipInfo,
);
return;
}
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (_) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
}
_clearSetTimeoutTimer() {
if (_setTimeoutTimer != null) {
_setTimeoutTimer?.cancel();
_setTimeoutTimer = null;
}
}
@@ -122,7 +147,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
: ipInfo != null
? Container(
alignment: Alignment.centerLeft,
height: globalState.appController
height: globalState
.measure.titleMediumHeight,
child: Text(
countryCodeToEmoji(
@@ -131,7 +156,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
.textTheme
.titleLarge
?.copyWith(
fontFamily: "Twemoji",
fontFamily: FontFamily.twEmoji.value,
),
),
)
@@ -150,9 +175,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
),
),
Container(
height: globalState.appController.measure.titleLargeHeight +
24 -
2,
height: globalState.measure.titleLargeHeight + 24 - 2,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: FadeBox(

View File

@@ -59,7 +59,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
style: bodyMedium,
maxLines: 1,
);
final size = globalState.appController.measure.computeTextSize(valueText);
final size = globalState.measure.computeTextSize(valueText);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -116,8 +116,8 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp,
),
child: Selector<AppState, List<Traffic>>(
selector: (_, appState) => appState.traffics,
child: Selector<AppFlowingState, List<Traffic>>(
selector: (_, appFlowingState) => appFlowingState.traffics,
builder: (_, traffics, __) {
return Container(
padding: const EdgeInsets.all(16),

View File

@@ -34,7 +34,7 @@ class _StartButtonState extends State<StartButton>
handleSwitchStart() {
final appController = globalState.appController;
if (isStart == appController.appState.isStart) {
if (isStart == appController.appFlowingState.isStart) {
isStart = !isStart;
updateController();
appController.updateStatus(isStart);
@@ -50,8 +50,8 @@ class _StartButtonState extends State<StartButton>
}
Widget _updateControllerContainer(Widget child) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.isStart,
return Selector<AppFlowingState, bool>(
selector: (_, appFlowingState) => appFlowingState.isStart,
builder: (_, isStart, child) {
if (isStart != this.isStart) {
this.isStart = isStart;
@@ -74,7 +74,7 @@ class _StartButtonState extends State<StartButton>
if (!state.isInit || !state.hasProfile) {
return Container();
}
final textWidth = globalState.appController.measure
final textWidth = globalState.measure
.computeTextSize(
Text(
other.getTimeDifference(
@@ -127,8 +127,8 @@ class _StartButtonState extends State<StartButton>
);
},
child: _updateControllerContainer(
Selector<AppState, int?>(
selector: (_, appState) => appState.runTime,
Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(

View File

@@ -5,34 +5,34 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class VPNSwitch extends StatelessWidget {
const VPNSwitch({super.key});
@override
Widget build(BuildContext context) {
return SwitchContainer(
info: const Info(
label: "VPN",
iconData: Icons.stacked_line_chart,
),
child: Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: enable,
onChanged: (value) {
final config = globalState.appController.config;
config.vpnProps = config.vpnProps.copyWith(
enable: value,
);
},
);
},
),
);
}
}
// class VPNSwitch extends StatelessWidget {
// const VPNSwitch({super.key});
//
// @override
// Widget build(BuildContext context) {
// return SwitchContainer(
// info: const Info(
// label: "VPN",
// iconData: Icons.stacked_line_chart,
// ),
// child: Selector<Config, bool>(
// selector: (_, config) => config.vpnProps.enable,
// builder: (_, enable, __) {
// return Switch(
// materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
// value: enable,
// onChanged: (value) {
// final config = globalState.appController.config;
// config.vpnProps = config.vpnProps.copyWith(
// enable: value,
// );
// },
// );
// },
// ),
// );
// }
// }
class TUNSwitch extends StatelessWidget {
const TUNSwitch({super.key});
@@ -47,14 +47,16 @@ class TUNSwitch extends StatelessWidget {
child: 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,
);
},
return LocaleBuilder(
builder: (_) => Switch(
value: enable,
onChanged: (value) {
final clashConfig = globalState.appController.clashConfig;
clashConfig.tun = clashConfig.tun.copyWith(
enable: value,
);
},
),
);
},
),
@@ -75,14 +77,16 @@ class ProxySwitch extends StatelessWidget {
child: Selector<Config, bool>(
selector: (_, config) => config.desktopProps.systemProxy,
builder: (_, systemProxy, __) {
return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.desktopProps =
config.desktopProps.copyWith(systemProxy: value);
},
return LocaleBuilder(
builder: (_) => Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.desktopProps =
config.desktopProps.copyWith(systemProxy: value);
},
),
);
},
),

View File

@@ -56,8 +56,8 @@ class TrafficUsage extends StatelessWidget {
label: appLocalizations.trafficUsage,
iconData: Icons.data_saver_off,
),
child: Selector<AppState, Traffic>(
selector: (_, appState) => appState.totalTraffic,
child: Selector<AppFlowingState, Traffic>(
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
builder: (_, totalTraffic, __) {
final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down;

View File

@@ -5,9 +5,9 @@ export 'profiles/profiles.dart';
export 'logs.dart';
export 'connections.dart';
export 'access.dart';
export 'config.dart';
export 'config/config.dart';
export 'application_setting.dart';
export 'about.dart';
export 'backup_and_recovery.dart';
export 'resources.dart';
export 'requests.dart';
export 'requests.dart';

250
lib/fragments/hotkey.dart Normal file
View File

@@ -0,0 +1,250 @@
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/card.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
extension IntlExt on Intl {
static actionMessage(String messageText) =>
Intl.message("action_$messageText");
}
class HotKeyFragment extends StatelessWidget {
const HotKeyFragment({super.key});
String getSubtitle(HotKeyAction hotKeyAction) {
final key = hotKeyAction.key;
if (key == null) {
return appLocalizations.noHotKey;
}
final modifierLabels =
hotKeyAction.modifiers.map((item) => item.physicalKeys.first.label);
var text = "";
if (modifierLabels.isNotEmpty) {
text += "${modifierLabels.join(" ")}+";
}
text += PhysicalKeyboardKey(key).label;
return text;
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: HotAction.values.length,
itemBuilder: (_, index) {
final hotAction = HotAction.values[index];
return Selector<Config, HotKeyAction>(
selector: (_, config) {
final index = config.hotKeyActions.indexWhere(
(item) => item.action == hotAction,
);
return index != -1
? config.hotKeyActions[index]
: HotKeyAction(
action: hotAction,
);
},
builder: (_, value, __) {
return ListItem(
title: Text(IntlExt.actionMessage(hotAction.name)),
subtitle: Text(
getSubtitle(value),
style: context.textTheme.bodyMedium
?.copyWith(color: context.colorScheme.primary),
),
onTap: () {
globalState.showCommonDialog(
child: HotKeyRecorder(
hotKeyAction: value,
),
);
},
);
},
);
},
);
}
}
class HotKeyRecorder extends StatefulWidget {
final HotKeyAction hotKeyAction;
const HotKeyRecorder({
super.key,
required this.hotKeyAction,
});
@override
State<HotKeyRecorder> createState() => _HotKeyRecorderState();
}
class _HotKeyRecorderState extends State<HotKeyRecorder> {
late ValueNotifier<HotKeyAction> hotKeyActionNotifier;
@override
void initState() {
super.initState();
hotKeyActionNotifier = ValueNotifier<HotKeyAction>(
widget.hotKeyAction.copyWith(),
);
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
}
bool _handleKeyEvent(KeyEvent keyEvent) {
if (keyEvent is KeyUpEvent) return false;
final keys = HardwareKeyboard.instance.physicalKeysPressed;
final key = keyEvent.physicalKey;
final modifiers = KeyboardModifier.values
.where((e) =>
e.physicalKeys.any(keys.contains) && !e.physicalKeys.contains(key))
.toSet();
hotKeyActionNotifier.value = hotKeyActionNotifier.value.copyWith(
modifiers: modifiers,
key: key.usbHidUsage,
);
return true;
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
super.dispose();
}
_handleRemove() {
Navigator.of(context).pop();
final config = globalState.appController.config;
config.updateOrAddHotKeyAction(
hotKeyActionNotifier.value.copyWith(
modifiers: {},
key: null,
),
);
}
_handleConfirm() {
Navigator.of(context).pop();
final config = globalState.appController.config;
final currentHotkeyAction = hotKeyActionNotifier.value;
if (currentHotkeyAction.key == null ||
currentHotkeyAction.modifiers.isEmpty) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.inputCorrectHotkey),
);
return;
}
final hotKeyActions = config.hotKeyActions;
final index = hotKeyActions.indexWhere(
(item) =>
item.key == currentHotkeyAction.key &&
keyboardModifierListEquality.equals(
item.modifiers,
currentHotkeyAction.modifiers,
),
);
if (index != -1) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.hotkeyConflict),
);
return;
}
config.updateOrAddHotKeyAction(
currentHotkeyAction,
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))),
content: ValueListenableBuilder(
valueListenable: hotKeyActionNotifier,
builder: (_, hotKeyAction, ___) {
final key = hotKeyAction.key;
final modifiers = hotKeyAction.modifiers;
return SizedBox(
width: dialogCommonWidth,
child: key != null
? Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final modifier in modifiers)
KeyboardKeyBox(
keyboardKey: modifier.physicalKeys.first,
),
if (modifiers.isNotEmpty)
Text(
"+",
style: context.textTheme.titleMedium,
),
KeyboardKeyBox(
keyboardKey: PhysicalKeyboardKey(key),
),
],
)
: Text(
appLocalizations.pressKeyboard,
style: context.textTheme.titleMedium,
),
);
},
),
actions: [
TextButton(
onPressed: () {
_handleRemove();
},
child: Text(appLocalizations.remove),
),
const SizedBox(
width: 8,
),
TextButton(
onPressed: () {
_handleConfirm();
},
child: Text(
appLocalizations.confirm,
),
),
],
);
}
}
class KeyboardKeyBox extends StatelessWidget {
final KeyboardKey keyboardKey;
const KeyboardKeyBox({
super.key,
required this.keyboardKey,
});
@override
Widget build(BuildContext context) {
return CommonCard(
type: CommonCardType.filled,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
keyboardKey.label,
style: const TextStyle(
fontSize: 16,
),
),
),
onPressed: () {},
);
}
}

View File

@@ -1,6 +1,4 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -21,7 +19,6 @@ class _LogsFragmentState extends State<LogsFragment> {
final scrollController = ScrollController(
keepScrollOffset: false,
);
List<GlobalObjectKey<_LogItemState>> keys = [];
Timer? timer;
@@ -29,19 +26,22 @@ class _LogsFragmentState extends State<LogsFragment> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
final appFlowingState = globalState.appController.appFlowingState;
logsNotifier.value =
logsNotifier.value.copyWith(logs: appFlowingState.logs);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final logs = appState.logs;
if (!const ListEquality<Log>().equals(
final logs = appFlowingState.logs;
if (!logListEquality.equals(
logsNotifier.value.logs,
logs,
)) {
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
logsNotifier.value = logsNotifier.value.copyWith(
logs: logs,
);
}
});
});
@@ -56,6 +56,21 @@ class _LogsFragmentState extends State<LogsFragment> {
timer = null;
}
_handleExport() async {
final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>(
() async {
return await globalState.appController.exportLogs();
},
title: appLocalizations.exportLogs,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.exportSuccess),
);
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
@@ -72,6 +87,17 @@ class _LogsFragmentState extends State<LogsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
});
}
@@ -112,15 +138,26 @@ class _LogsFragmentState extends State<LogsFragment> {
child: ValueListenableBuilder<LogsAndKeywords>(
valueListenable: logsNotifier,
builder: (_, state, __) {
var logs = state.filteredLogs;
final logs = state.filteredLogs;
if (logs.isEmpty) {
return NullStatus(
label: appLocalizations.nullLogsDesc,
);
}
logs = logs.reversed.toList();
keys = logs
.map((log) => GlobalObjectKey<_LogItemState>(log.dateTime))
final reversedLogs = logs.reversed.toList();
final logWidgets = reversedLogs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: _addKeyword,
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -147,22 +184,38 @@ class _LogsFragmentState extends State<LogsFragment> {
),
),
Expanded(
child: ListView.separated(
controller: scrollController,
itemBuilder: (_, index) {
final log = logs[index];
return LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: _addKeyword,
);
child: LayoutBuilder(
builder: (_, constraints) {
return ScrollConfiguration(
behavior: ShowBarScrollBehavior(),
child: ListView.builder(
controller: scrollController,
itemExtentBuilder: (index, __) {
final widget = logWidgets[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyLargeSize = measure.bodyLargeSize;
final bodySmallHeight = measure.bodySmallHeight;
final bodyMediumHeight = measure.bodyMediumHeight;
final log = reversedLogs[(index / 2).floor()];
final width = (log.payload?.length ?? 0) *
bodyLargeSize.width +
200;
final lines = (width / constraints.maxWidth).ceil();
return lines * bodyLargeSize.height +
bodySmallHeight +
8 +
bodyMediumHeight +
40;
},
itemBuilder: (_, index) {
return logWidgets[index];
},
itemCount: logWidgets.length,
));
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: logs.length,
),
)
],
@@ -236,7 +289,8 @@ class LogsSearchDelegate extends SearchDelegate {
_addKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)..add(keyword);
final keywords = List<String>.from(logsNotifier.value.keywords)
..add(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
@@ -245,7 +299,8 @@ class LogsSearchDelegate extends SearchDelegate {
_deleteKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)..remove(keyword);
final keywords = List<String>.from(logsNotifier.value.keywords)
..remove(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
@@ -330,7 +385,9 @@ class _LogItemState extends State<LogItem> {
horizontal: 16,
vertical: 4,
),
title: SelectableText(log.payload ?? ''),
title: SelectableText(
log.payload ?? '',
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -339,7 +396,9 @@ class _LogItemState extends State<LogItem> {
style: context.textTheme.bodySmall
?.copyWith(color: context.colorScheme.primary),
),
const SizedBox(height: 8,),
const SizedBox(
height: 8,
),
Container(
alignment: Alignment.centerLeft,
child: CommonChip(

View File

@@ -46,6 +46,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
final messages = [];
final updateProfiles = profiles.map<Future>(
(profile) async {
if (profile.type == ProfileType.file) return;
config.setProfile(
profile.copyWith(isUpdating: true),
);
@@ -458,10 +459,9 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
flex: 1,
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 12),
proxyDecorator: proxyDecorator,
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == newIndex) return;
onReorder: (oldIndex, newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
@@ -494,26 +494,29 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
itemCount: profiles.length,
),
),
Padding(
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
vertical: 16,
horizontal: 24,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.config.profiles = profiles;
},
icon: const Icon(
Icons.check,
),
iconSize: 32,
padding: const EdgeInsets.all(8),
child: FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
globalState.appController.config.profiles = profiles;
},
style: ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(vertical: 8),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
appLocalizations.confirm,
),
],
),
),
),
],

View File

@@ -166,20 +166,20 @@ class ContextMenuControllerImpl implements SelectionToolbarController {
// _removeOverLayEntry();
}
_handleCut(CodeLineEditingController controller) {
controller.cut();
_removeOverLayEntry();
}
_handleCopy(CodeLineEditingController controller) async {
await controller.copy();
_removeOverLayEntry();
}
_handlePaste(CodeLineEditingController controller) {
controller.paste();
_removeOverLayEntry();
}
// _handleCut(CodeLineEditingController controller) {
// controller.cut();
// _removeOverLayEntry();
// }
//
// _handleCopy(CodeLineEditingController controller) async {
// await controller.copy();
// _removeOverLayEntry();
// }
//
// _handlePaste(CodeLineEditingController controller) {
// controller.paste();
// _removeOverLayEntry();
// }
@override
void show({

View File

@@ -23,7 +23,11 @@ class ProxyCard extends StatelessWidget {
required this.type,
});
Measure get measure => globalState.appController.measure;
Measure get measure => globalState.measure;
_handleTestCurrentDelay() {
proxyDelayTest(proxy);
}
Widget _buildDelayText() {
return SizedBox(
@@ -36,24 +40,31 @@ class ProxyCard extends StatelessWidget {
return FadeBox(
child: Builder(
builder: (_) {
if (delay == null) {
return Container();
}
if (delay == 0) {
if (delay == 0 || delay == null) {
return SizedBox(
height: measure.labelSmallHeight,
width: measure.labelSmallHeight,
child: const CircularProgressIndicator(
strokeWidth: 2,
),
child: delay == 0
? const CircularProgressIndicator(
strokeWidth: 2,
)
: IconButton(
icon: const Icon(Icons.bolt),
iconSize: globalState.measure.labelSmallHeight,
padding: EdgeInsets.zero,
onPressed: _handleTestCurrentDelay,
),
);
}
return Text(
delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: other.getDelayColor(
delay,
return GestureDetector(
onTap: _handleTestCurrentDelay,
child: Text(
delay > 0 ? '$delay ms' : "Timeout",
style: context.textTheme.labelSmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: other.getDelayColor(
delay,
),
),
),
);
@@ -91,12 +102,12 @@ class ProxyCard extends StatelessWidget {
_changeProxy(BuildContext context) async {
final appController = globalState.appController;
final isUrlTest = groupType == GroupType.URLTest;
final isURLTestOrFallback = groupType.isURLTestOrFallback;
final isSelector = groupType == GroupType.Selector;
if (isUrlTest || isSelector) {
if (isURLTestOrFallback || isSelector) {
final currentProxyName =
appController.config.currentSelectedMap[groupName];
final nextProxyName = switch (isUrlTest) {
final nextProxyName = switch (isURLTestOrFallback) {
true => currentProxyName == proxy.name ? "" : proxy.name,
false => proxy.name,
};
@@ -119,10 +130,10 @@ class ProxyCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final measure = globalState.appController.measure;
final measure = globalState.measure;
final delayText = _buildDelayText();
final proxyNameText = _buildProxyNameText(context);
return currentGroupProxyNameBuilder(
return currentSelectedProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
return Stack(
@@ -196,30 +207,16 @@ class ProxyCard extends StatelessWidget {
),
),
),
if (groupType == GroupType.URLTest)
if (groupType.isURLTestOrFallback)
Selector<Config, String>(
selector: (_, config) {
final selectedProxyName =
config.currentSelectedMap[groupName];
return selectedProxyName ?? '';
},
builder: (_, value, __) {
builder: (_, value, child) {
if (value != proxy.name) return Container();
return Positioned.fill(
child: Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
),
),
);
return child!;
},
child: Positioned.fill(
child: Container(

View File

@@ -1,14 +1,12 @@
import 'dart:math';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/other.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';
Widget currentGroupProxyNameBuilder({
Widget currentSelectedProxyNameBuilder({
required String groupName,
required Widget Function(String currentGroupName) builder,
}) {
@@ -18,19 +16,19 @@ Widget currentGroupProxyNameBuilder({
final selectedProxyName = config.currentSelectedMap[groupName];
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
},
builder: (_, currentGroupName, ___) {
return builder(currentGroupName);
builder: (_, currentSelectedProxyName, ___) {
return builder(currentSelectedProxyName);
},
);
}
double get listHeaderHeight {
final measure = globalState.appController.measure;
final measure = globalState.measure;
return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
}
double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.appController.measure;
final measure = globalState.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
return switch (proxyCardType) {
@@ -40,10 +38,26 @@ double getItemHeight(ProxyCardType proxyCardType) {
};
}
proxyDelayTest(Proxy proxy) async {
final appController = globalState.appController;
final proxyName = appController.appState.getRealProxyName(proxy.name);
globalState.appController.setDelay(
Delay(
name: proxyName,
value: 0,
),
);
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
}
delayTest(List<Proxy> proxies) async {
final appController = globalState.appController;
final delayProxies = proxies.map<Future>((proxy) async {
final proxyName = appController.appState.getRealProxyName(proxy.name);
final proxyNames = proxies
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
.toSet()
.toList();
final delayProxies = proxyNames.map<Future>((proxyName) async {
globalState.appController.setDelay(
Delay(
name: proxyName,
@@ -51,8 +65,12 @@ delayTest(List<Proxy> proxies) async {
),
);
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
});
await Future.wait(delayProxies);
}).toList();
final batchesDelayProxies = delayProxies.batch(100);
for (final batchDelayProxies in batchesDelayProxies) {
await Future.wait(batchDelayProxies);
}
appController.appState.sortNum++;
}
@@ -63,14 +81,14 @@ double getScrollToSelectedOffset({
final appController = globalState.appController;
final columns = other.getProxiesColumns(
appController.appState.viewWidth,
appController.config.proxiesLayout,
appController.config.proxiesStyle.layout,
);
final proxyCardType = appController.config.proxyCardType;
final proxyCardType = appController.config.proxiesStyle.cardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere(
(proxy) => proxy.name == selectedName,
);
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
final rows = (selectedIndex / columns).floor();
return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0);
return rows * getItemHeight(proxyCardType) + (rows - 1) * 8;
}

View File

@@ -1,10 +1,10 @@
import 'package:collection/collection.dart';
import 'dart:math';
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/card.dart';
import 'package:fl_clash/widgets/text.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -217,11 +217,15 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
final currentInitOffset = _headerOffset[index];
final proxies = _lastGroupNameProxiesMap[groupName];
_controller.animateTo(
currentInitOffset +
getScrollToSelectedOffset(
groupName: groupName,
proxies: proxies ?? [],
),
min(
currentInitOffset +
8 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: proxies ?? [],
),
_controller.position.maxScrollExtent,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
@@ -236,18 +240,17 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return ProxiesListSelectorState(
groupNames: groupNames,
currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
proxyCardType: config.proxiesStyle.cardType,
proxiesSortType: config.proxiesStyle.sortType,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
config.proxiesStyle.layout,
),
sortNum: appState.sortNum,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
offset: 0,
currentIndex: 0,
@@ -353,6 +356,8 @@ class _ListHeaderState extends State<ListHeader>
late Animation<double> _iconTurns;
var isLock = false;
String get icon => widget.group.icon;
String get groupName => widget.group.name;
String get groupType => widget.group.type.name;
@@ -367,11 +372,6 @@ class _ListHeaderState extends State<ListHeader>
}
_handleChange(String groupName) {
if (isExpand) {
_animationController.reverse();
} else {
_animationController.forward();
}
widget.onChange(groupName);
}
@@ -401,75 +401,147 @@ class _ListHeaderState extends State<ListHeader>
super.didUpdateWidget(oldWidget);
if (oldWidget.isExpand != widget.isExpand) {
if (isExpand) {
_animationController.value = 1.0;
_animationController.forward();
} else {
_animationController.value = 0.0;
_animationController.reverse();
}
}
}
Widget _buildIcon() {
return Selector<Config, ProxiesIconStyle>(
selector: (_, config) => config.proxiesStyle.iconStyle,
builder: (_, iconStyle, child) {
return Selector<Config, String>(
selector: (_, config) {
final iconMapEntryList =
config.proxiesStyle.iconMap.entries.toList();
final index = iconMapEntryList.indexWhere((item) {
try{
return RegExp(item.key).hasMatch(groupName);
}catch(_){
return false;
}
});
if (index != -1) {
return iconMapEntryList[index].value;
}
return icon;
},
builder: (_, icon, __) {
return switch (iconStyle) {
ProxiesIconStyle.standard => Container(
height: 48,
width: 48,
margin: const EdgeInsets.only(
right: 16,
),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonIcon(
src: icon,
size: 32,
),
),
ProxiesIconStyle.icon => Container(
margin: const EdgeInsets.only(
right: 16,
),
child: CommonIcon(
src: icon,
size: 42,
),
),
ProxiesIconStyle.none => Container(),
};
},
);
},
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
key: widget.key,
radius: 24,
type: CommonCardType.filled,
child: Container(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Text(
groupName,
style: context.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
_buildIcon(),
Flexible(
flex: 1,
child: Row(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
groupType,
style: context.textTheme.labelMedium?.toLight,
groupName,
style: context.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Flexible(
flex: 1,
child: currentGroupProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (currentGroupName.isNotEmpty) ...[
Flexible(
flex: 1,
child: EmojiText(
overflow: TextOverflow.ellipsis,
" · $currentGroupName",
style: context
.textTheme.labelMedium?.toLight,
),
),
]
],
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
groupType,
style: context.textTheme.labelMedium?.toLight,
),
Flexible(
flex: 1,
child: currentSelectedProxyNameBuilder(
groupName: groupName,
builder: (currentGroupName) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
if (currentGroupName.isNotEmpty) ...[
Flexible(
flex: 1,
child: EmojiText(
overflow: TextOverflow.ellipsis,
" · $currentGroupName",
style: context.textTheme
.labelMedium?.toLight,
),
),
]
],
);
},
),
),
],
),
),
const SizedBox(
width: 4,
),
],
),
),
)
],
),
),

View File

@@ -68,19 +68,30 @@ class _ProvidersState extends State<Providers> {
return Selector<AppState, List<ExternalProvider>>(
selector: (_, appState) => appState.providers,
builder: (_, providers, ___) {
return ListView.separated(
itemBuilder: (_, index) {
return ProviderItem(
provider: providers[index],
);
},
separatorBuilder: (_, index) {
return const Divider(
height: 0,
);
},
itemCount: providers.length,
final proxyProviders =
providers.where((item) => item.type == "Proxy").map(
(item) => ProviderItem(
provider: item,
),
);
final ruleProviders =
providers.where((item) => item.type == "Rule").map(
(item) => ProviderItem(
provider: item,
),
);
final proxySection = generateSection(
title: appLocalizations.proxyProviders,
items: proxyProviders,
);
final ruleSection = generateSection(
title: appLocalizations.ruleProviders,
items: ruleProviders,
);
return generateListView([
...proxySection,
...ruleSection,
]);
},
);
}

View File

@@ -1,11 +1,11 @@
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/fragments/proxies/list.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 'providers.dart';
import 'setting.dart';
import 'tab.dart';
@@ -29,15 +29,15 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
IconButton(
onPressed: () {
showExtendPage(
forceNotSide: true,
isScaffold: true,
extendPageWidth: 360,
context,
body: const Providers(),
title: appLocalizations.externalResources,
title: appLocalizations.providers,
);
},
icon: const Icon(
Icons.swap_vert_circle_outlined,
Icons.poll_outlined,
),
),
const SizedBox(
@@ -56,6 +56,63 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
const SizedBox(
width: 8,
)
] else ...[
IconButton(
onPressed: () {
showExtendPage(
context,
extendPageWidth: 360,
title: appLocalizations.iconConfiguration,
body: Selector<Config, Map<String, String>>(
selector: (_, config) => config.proxiesStyle.iconMap,
shouldRebuild: (prev, next) {
return !stringAndStringMapEntryIterableEquality.equals(
prev.entries,
next.entries,
);
},
builder: (_, iconMap, __) {
final entries = iconMap.entries.toList();
return ListPage(
title: appLocalizations.iconConfiguration,
items: entries,
keyLabel: appLocalizations.regExp,
valueLabel: appLocalizations.icon,
keyBuilder: (item) => Key(item.key),
titleBuilder: (item) => Text(item.key),
leadingBuilder: (item) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonIcon(
src: item.value,
size: 42,
),
),
subtitleBuilder: (item) => Text(
item.value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onChange: (entries) {
final config = globalState.appController.config;
config.proxiesStyle = config.proxiesStyle.copyWith(
iconMap: Map.fromEntries(entries),
);
},
);
},
),
);
},
icon: const Icon(
Icons.style_outlined,
),
),
const SizedBox(
width: 8,
)
],
IconButton(
onPressed: () {
@@ -63,7 +120,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
title: appLocalizations.proxiesSetting,
context: context,
builder: (context) {
return const ProxiesSettingWidget();
return const ProxiesSetting();
},
);
},
@@ -78,7 +135,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
@override
Widget build(BuildContext context) {
return Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
selector: (_, config) => config.proxiesStyle.type,
builder: (_, proxiesType, __) {
return ProxiesActionsBuilder(
builder: (state, child) {

View File

@@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProxiesSettingWidget extends StatelessWidget {
const ProxiesSettingWidget({super.key});
class ProxiesSetting extends StatelessWidget {
const ProxiesSetting({super.key});
IconData _getIconWithProxiesType(ProxiesType type) {
return switch (type) {
@@ -41,6 +41,14 @@ class ProxiesSettingWidget extends StatelessWidget {
};
}
String _getTextWithProxiesIconStyle(ProxiesIconStyle style) {
return switch (style) {
ProxiesIconStyle.standard => appLocalizations.standard,
ProxiesIconStyle.none => appLocalizations.noIcon,
ProxiesIconStyle.icon => appLocalizations.onlyIcon,
};
}
List<Widget> _buildStyleSetting() {
return generateSection(
title: appLocalizations.style,
@@ -49,7 +57,7 @@ class ProxiesSettingWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
selector: (_, config) => config.proxiesStyle.type,
builder: (_, proxiesType, __) {
final config = globalState.appController.config;
return Wrap(
@@ -63,7 +71,9 @@ class ProxiesSettingWidget extends StatelessWidget {
),
isSelected: proxiesType == item,
onPressed: () {
config.proxiesType = item;
config.proxiesStyle = config.proxiesStyle.copyWith(
type: item,
);
},
)
],
@@ -83,7 +93,7 @@ class ProxiesSettingWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesSortType>(
selector: (_, config) => config.proxiesSortType,
selector: (_, config) => config.proxiesStyle.sortType,
builder: (_, proxiesSortType, __) {
final config = globalState.appController.config;
return Wrap(
@@ -97,7 +107,9 @@ class ProxiesSettingWidget extends StatelessWidget {
),
isSelected: proxiesSortType == item,
onPressed: () {
config.proxiesSortType = item;
config.proxiesStyle = config.proxiesStyle.copyWith(
sortType: item,
);
},
),
],
@@ -117,7 +129,7 @@ class ProxiesSettingWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxyCardType>(
selector: (_, config) => config.proxyCardType,
selector: (_, config) => config.proxiesStyle.cardType,
builder: (_, proxyCardType, __) {
final config = globalState.appController.config;
return Wrap(
@@ -128,7 +140,9 @@ class ProxiesSettingWidget extends StatelessWidget {
Intl.message(item.name),
isSelected: item == proxyCardType,
onPressed: () {
config.proxyCardType = item;
config.proxiesStyle = config.proxiesStyle.copyWith(
cardType: item,
);
},
)
],
@@ -149,8 +163,8 @@ class ProxiesSettingWidget extends StatelessWidget {
horizontal: 16,
),
scrollDirection: Axis.horizontal,
child: Selector< Config, ProxiesLayout>(
selector: (_, config) => config.proxiesLayout,
child: Selector<Config, ProxiesLayout>(
selector: (_, config) => config.proxiesStyle.layout,
builder: (_, proxiesLayout, __) {
final config = globalState.appController.config;
return Wrap(
@@ -161,7 +175,9 @@ class ProxiesSettingWidget extends StatelessWidget {
getTextForProxiesLayout(item),
isSelected: item == proxiesLayout,
onPressed: () {
config.proxiesLayout = item;
config.proxiesStyle = config.proxiesStyle.copyWith(
layout: item,
);
},
)
],
@@ -173,6 +189,39 @@ class ProxiesSettingWidget extends StatelessWidget {
);
}
_buildGroupStyleSetting() {
return generateSection(
title: "图标样式",
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesIconStyle>(
selector: (_, config) => config.proxiesStyle.iconStyle,
builder: (_, iconStyle, __) {
return Wrap(
spacing: 16,
children: [
for (final item in ProxiesIconStyle.values)
SettingTextCard(
_getTextWithProxiesIconStyle(item),
isSelected: iconStyle == item,
onPressed: () {
final config = globalState.appController.config;
config.proxiesStyle = config.proxiesStyle.copyWith(
iconStyle: item,
);
},
),
],
);
},
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Padding(
@@ -185,6 +234,22 @@ class ProxiesSettingWidget extends StatelessWidget {
..._buildSortSetting(),
..._buildLayoutSetting(),
..._buildSizeSetting(),
Selector<Config, bool>(
selector: (_, config) =>
config.proxiesStyle.type == ProxiesType.list,
builder: (_, value, child) {
if (value) {
return child!;
}
return Container();
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildGroupStyleSetting(),
],
),
),
],
),
);

View File

@@ -1,4 +1,4 @@
import 'package:collection/collection.dart';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -118,8 +118,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_tabController?.dispose();
_tabController = null;
return true;
@@ -138,7 +137,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
return KeepContainer(
return KeepScope(
child: ProxyGroupView(
key: keyMap[groupName],
groupName: groupName,
@@ -266,11 +265,14 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
return;
}
_controller.animateTo(
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: _lastProxies,
),
min(
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: _lastProxies,
),
_controller.position.maxScrollExtent,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
@@ -282,11 +284,11 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
proxyCardType: config.proxiesStyle.cardType,
proxiesSortType: config.proxiesStyle.sortType,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesLayout,
config.proxiesStyle.layout,
),
sortNum: appState.sortNum,
proxies: group.all,
@@ -311,7 +313,12 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
alignment: Alignment.topCenter,
child: GridView.builder(
controller: _controller,
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: 96,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -37,8 +36,11 @@ class _RequestsFragmentState extends State<RequestsFragment> {
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final requests = appState.requests;
if (!const ListEquality<Connection>().equals(
final maxLength = Platform.isAndroid ? 1000 : 60;
final requests = appState.requests.safeSublist(
appState.requests.length - maxLength,
);
if (!connectionListEquality.equals(
requestsNotifier.value.connections,
requests,
)) {

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