Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196a50aaf7 |
57
.github/release_template.md
vendored
57
.github/release_template.md
vendored
@@ -1,57 +0,0 @@
|
||||
<div align=center>
|
||||
|
||||
[](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>
|
||||
@@ -35,6 +35,7 @@ jobs:
|
||||
install: mingw-w64-x86_64-gcc
|
||||
update: true
|
||||
|
||||
|
||||
- name: Set Mingw64 Env
|
||||
if: startsWith(matrix.platform,'windows')
|
||||
run: |
|
||||
@@ -79,7 +80,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version-file: 'core/go.mod'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
|
||||
@@ -101,21 +102,15 @@ jobs:
|
||||
with:
|
||||
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
|
||||
path: ./dist
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
|
||||
|
||||
upload:
|
||||
upload-release:
|
||||
if: ${{ !contains(github.ref, '+') }}
|
||||
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
|
||||
@@ -129,72 +124,34 @@ jobs:
|
||||
pattern: artifact-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate release.md
|
||||
- name: Pre Release
|
||||
run: |
|
||||
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
|
||||
|
||||
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
|
||||
- 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 }}
|
||||
with:
|
||||
source-directory: ./tmp/
|
||||
source-directory: ./dist/
|
||||
destination-github-username: chen08209
|
||||
destination-repository-name: FlClash-fdroid-repo
|
||||
user-name: 'github-actions[bot]'
|
||||
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/
|
||||
64
.github/workflows/change.yaml
vendored
64
.github/workflows/change.yaml
vendored
@@ -1,64 +0,0 @@
|
||||
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 --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
||||
currentTag=""
|
||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||
if (( i < ${#tags[@]} )); then
|
||||
tag=${tags[$i]}
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
echo "## $currentTag" >> CHANGELOG.md
|
||||
echo "" >> CHANGELOG.md
|
||||
if [ -n "$tag" ]; then
|
||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> CHANGELOG.md
|
||||
else
|
||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> CHANGELOG.md
|
||||
fi
|
||||
echo "" >> CHANGELOG.md
|
||||
fi
|
||||
currentTag=$tag
|
||||
done
|
||||
|
||||
- name: Commit
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "Commit pushing"
|
||||
git config --local user.email "chen08209@gmail.com"
|
||||
git config --local user.name "chen08209"
|
||||
git commit -m "Update changelog"
|
||||
git push
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Push succeeded"
|
||||
else
|
||||
echo "Push failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,7 +1,7 @@
|
||||
[submodule "core/Clash.Meta"]
|
||||
path = core/Clash.Meta
|
||||
url = git@github.com:chen08209/Clash.Meta.git
|
||||
branch = FlClash-Alpha
|
||||
branch = FlClash
|
||||
[submodule "plugins/flutter_distributor"]
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
|
||||
664
CHANGELOG.md
664
CHANGELOG.md
@@ -1,664 +0,0 @@
|
||||
## v0.8.67
|
||||
|
||||
- Add android shortcuts
|
||||
|
||||
- Fix init params issues
|
||||
|
||||
- Fix dynamic color issues
|
||||
|
||||
- Optimize navigator animate
|
||||
|
||||
- Optimize window init
|
||||
|
||||
- Optimize fab
|
||||
|
||||
- Optimize save
|
||||
|
||||
## v0.8.66
|
||||
|
||||
- Fix the collapse issues
|
||||
|
||||
- Add fontFamily options
|
||||
|
||||
## v0.8.65
|
||||
|
||||
- Update core version
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Optimize ip check
|
||||
|
||||
- Optimize url-test
|
||||
|
||||
## v0.8.64
|
||||
|
||||
- Update release message
|
||||
|
||||
- Init auto gen changelog
|
||||
|
||||
- Fix windows tray issues
|
||||
|
||||
- Fix urltest issues
|
||||
|
||||
- Add auto changelog
|
||||
|
||||
- Fix windows admin auto launch issues
|
||||
|
||||
- Add android vpn options
|
||||
|
||||
- Support proxies icon configuration
|
||||
|
||||
- Optimize android immersion display
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- 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
|
||||
|
||||
## 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
|
||||
|
||||
17
README.md
17
README.md
@@ -6,9 +6,13 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/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>
|
||||
|
||||
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
|
||||
@@ -38,6 +42,10 @@ 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
|
||||
@@ -92,6 +100,9 @@ on Mobile:
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Star
|
||||
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/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>
|
||||
|
||||
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
||||
|
||||
@@ -39,6 +42,11 @@ 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
|
||||
|
||||
@@ -34,22 +34,22 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 34
|
||||
ndkVersion "27.1.12297006"
|
||||
ndkVersion "25.1.8937393"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
signingConfigs {
|
||||
if (isRelease) {
|
||||
if (isRelease){
|
||||
release {
|
||||
storeFile defStoreFile
|
||||
storePassword defStorePassword
|
||||
@@ -74,9 +74,10 @@ android {
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
release {
|
||||
if (isRelease) {
|
||||
minifyEnabled true
|
||||
if(isRelease){
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
}else{
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
@@ -101,9 +102,6 @@ flutter {
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
|
||||
exclude group: "com.google.guava", module: "guava"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash">
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:extractNativeLibs="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:label="FlClash"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
@@ -56,39 +56,30 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="clash" />
|
||||
<data android:scheme="clashmeta" />
|
||||
<data android:scheme="flclash" />
|
||||
<data android:scheme="clash"/>
|
||||
<data android:scheme="clashmeta"/>
|
||||
<data android:scheme="flclash"/>
|
||||
|
||||
<data android:host="install-config" />
|
||||
<data android:host="install-config"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
|
||||
<!-- android:value="true" />-->
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
|
||||
<!-- android:value="true" />-->
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/TransparentTheme">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.STOP" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.CHANGE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:theme="@style/TransparentTheme" />
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -123,22 +114,11 @@
|
||||
android:name=".services.FlClashVpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
>
|
||||
<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">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.follow.clash
|
||||
|
||||
|
||||
import com.follow.clash.models.VpnOptions
|
||||
|
||||
interface BaseServiceInterface {
|
||||
fun start(options: VpnOptions): Int
|
||||
fun stop()
|
||||
fun startForeground(title: String, content: String)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.ProxyPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
@@ -22,7 +22,6 @@ enum class RunState {
|
||||
object GlobalState {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
val runLock = ReentrantLock()
|
||||
|
||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
@@ -33,40 +32,11 @@ object GlobalState {
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun getText(text: String): String {
|
||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
||||
}
|
||||
|
||||
fun getCurrentTilePlugin(): TilePlugin? {
|
||||
fun getCurrentTitlePlugin(): TilePlugin? {
|
||||
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
||||
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
|
||||
}
|
||||
|
||||
fun getCurrentVPNPlugin(): VpnPlugin? {
|
||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
}
|
||||
|
||||
fun handleToggle(context: Context) {
|
||||
if (runState.value == RunState.STOP) {
|
||||
runState.value = RunState.PENDING
|
||||
val tilePlugin = getCurrentTilePlugin()
|
||||
if (tilePlugin != null) {
|
||||
tilePlugin.handleStart()
|
||||
} else {
|
||||
initServiceEngine(context)
|
||||
}
|
||||
} else {
|
||||
handleStop()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
if (runState.value == RunState.START) {
|
||||
runState.value = RunState.PENDING
|
||||
getCurrentTilePlugin()?.handleStop()
|
||||
}
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
serviceEngine?.destroy()
|
||||
serviceEngine = null
|
||||
@@ -77,10 +47,9 @@ object GlobalState {
|
||||
lock.withLock {
|
||||
destroyServiceEngine()
|
||||
serviceEngine = FlutterEngine(context)
|
||||
serviceEngine?.plugins?.add(VpnPlugin())
|
||||
serviceEngine?.plugins?.add(ProxyPlugin())
|
||||
serviceEngine?.plugins?.add(AppPlugin())
|
||||
serviceEngine?.plugins?.add(TilePlugin())
|
||||
serviceEngine?.plugins?.add(ServicePlugin())
|
||||
val vpnService = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||
"vpnService"
|
||||
|
||||
@@ -2,18 +2,17 @@ package com.follow.clash
|
||||
|
||||
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.ProxyPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
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())
|
||||
flutterEngine.plugins.add(VpnPlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin())
|
||||
flutterEngine.plugins.add(ProxyPlugin())
|
||||
flutterEngine.plugins.add(TilePlugin())
|
||||
GlobalState.flutterEngine = flutterEngine
|
||||
}
|
||||
|
||||
@@ -2,20 +2,10 @@ package com.follow.clash
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import com.follow.clash.extensions.wrapAction
|
||||
|
||||
class TempActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.action) {
|
||||
wrapAction("STOP") -> {
|
||||
GlobalState.handleStop()
|
||||
}
|
||||
|
||||
wrapAction("CHANGE") -> {
|
||||
GlobalState.handleToggle(applicationContext)
|
||||
}
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.follow.clash.extensions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.system.OsConstants.IPPROTO_TCP
|
||||
import android.system.OsConstants.IPPROTO_UDP
|
||||
import android.util.Base64
|
||||
import java.net.URL
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.follow.clash.models.Metadata
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = drawable.toBitmap()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
fun Metadata.getProtocol(): Int? {
|
||||
if (network.startsWith("tcp")) return IPPROTO_TCP
|
||||
if (network.startsWith("udp")) return IPPROTO_UDP
|
||||
return null
|
||||
}
|
||||
|
||||
fun String.getInetSocketAddress(): InetSocketAddress {
|
||||
val url = URL("https://$this")
|
||||
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package com.follow.clash.extensions
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.system.OsConstants.IPPROTO_TCP
|
||||
import android.system.OsConstants.IPPROTO_UDP
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.follow.clash.TempActivity
|
||||
import com.follow.clash.models.CIDR
|
||||
import com.follow.clash.models.Metadata
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = drawable.toBitmap()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
fun Metadata.getProtocol(): Int? {
|
||||
if (network.startsWith("tcp")) return IPPROTO_TCP
|
||||
if (network.startsWith("udp")) return IPPROTO_UDP
|
||||
return null
|
||||
}
|
||||
|
||||
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
|
||||
return routeAddress.filter {
|
||||
it.isIpv4()
|
||||
}.map {
|
||||
it.toCIDR()
|
||||
}
|
||||
}
|
||||
|
||||
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
|
||||
return routeAddress.filter {
|
||||
it.isIpv6()
|
||||
}.map {
|
||||
it.toCIDR()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isIpv4(): Boolean {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val address = InetAddress.getByName(parts[0])
|
||||
return address.address.size == 4
|
||||
}
|
||||
|
||||
fun String.isIpv6(): Boolean {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val address = InetAddress.getByName(parts[0])
|
||||
return address.address.size == 16
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.wrapAction(action: String): String {
|
||||
return "${this.packageName}.action.$action"
|
||||
}
|
||||
|
||||
fun Context.getActionIntent(action: String): Intent {
|
||||
val actionIntent = Intent(this, TempActivity::class.java)
|
||||
actionIntent.action = wrapAction(action)
|
||||
return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
}
|
||||
|
||||
fun Context.getActionPendingIntent(action: String): PendingIntent {
|
||||
return if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
getActionIntent(action),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
getActionIntent(action),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
suspend fun <T> MethodChannel.awaitResult(
|
||||
method: String,
|
||||
arguments: Any? = null
|
||||
): T? = withContext(Dispatchers.Main) { // 切换到主线程
|
||||
suspendCoroutine { continuation ->
|
||||
invokeMethod(method, arguments, object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
continuation.resume(result as T)
|
||||
}
|
||||
|
||||
override fun error(code: String, message: String?, details: Any?) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
|
||||
override fun notImplemented() {
|
||||
continuation.resume(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,5 @@ package com.follow.clash.models
|
||||
data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val isSystem: Boolean,
|
||||
val firstInstallTime: Long,
|
||||
val isSystem:Boolean
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import java.net.InetAddress
|
||||
|
||||
enum class AccessControlMode {
|
||||
acceptSelected, rejectSelected,
|
||||
acceptSelected,
|
||||
rejectSelected,
|
||||
}
|
||||
|
||||
data class AccessControl(
|
||||
@@ -12,17 +11,8 @@ data class AccessControl(
|
||||
val rejectList: List<String>,
|
||||
)
|
||||
|
||||
data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||
|
||||
data class VpnOptions(
|
||||
val enable: Boolean,
|
||||
val port: Int,
|
||||
data class Props(
|
||||
val accessControl: AccessControl?,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
val routeAddress: List<String>,
|
||||
val ipv4Address: String,
|
||||
val ipv6Address: String,
|
||||
val dnsServerAddress: String,
|
||||
)
|
||||
val allowBypass: Boolean?,
|
||||
val systemProxy: Boolean?,
|
||||
)
|
||||
|
||||
@@ -6,27 +6,19 @@ import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
import com.follow.clash.extensions.getActionIntent
|
||||
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
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
@@ -37,10 +29,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
@@ -48,96 +40,24 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private var toast: Toast? = null
|
||||
|
||||
private lateinit var context: Context
|
||||
private var context: Context? = null
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
private var vpnCallBack: (() -> Unit)? = null
|
||||
private var connectivity: ConnectivityManager? = null
|
||||
|
||||
private val iconMap = mutableMapOf<String, String?>()
|
||||
|
||||
private val packages = mutableListOf<Package>()
|
||||
|
||||
private val skipPrefixList = listOf(
|
||||
"com.google",
|
||||
"com.android.chrome",
|
||||
"com.android.vending",
|
||||
"com.microsoft",
|
||||
"com.apple",
|
||||
"com.zhiliaoapp.musically", // Banned by China
|
||||
)
|
||||
|
||||
private val chinaAppPrefixList = listOf(
|
||||
"com.tencent",
|
||||
"com.alibaba",
|
||||
"com.umeng",
|
||||
"com.qihoo",
|
||||
"com.ali",
|
||||
"com.alipay",
|
||||
"com.amap",
|
||||
"com.sina",
|
||||
"com.weibo",
|
||||
"com.vivo",
|
||||
"com.xiaomi",
|
||||
"com.huawei",
|
||||
"com.taobao",
|
||||
"com.secneo",
|
||||
"s.h.e.l.l",
|
||||
"com.stub",
|
||||
"com.kiwisec",
|
||||
"com.secshell",
|
||||
"com.wrapper",
|
||||
"cn.securitystack",
|
||||
"com.mogosec",
|
||||
"com.secoen",
|
||||
"com.netease",
|
||||
"com.mx",
|
||||
"com.qq.e",
|
||||
"com.baidu",
|
||||
"com.bytedance",
|
||||
"com.bugly",
|
||||
"com.miui",
|
||||
"com.oppo",
|
||||
"com.coloros",
|
||||
"com.iqoo",
|
||||
"com.meizu",
|
||||
"com.gionee",
|
||||
"cn.nubia",
|
||||
"com.oplus",
|
||||
"andes.oplus",
|
||||
"com.unionpay",
|
||||
"cn.wps"
|
||||
)
|
||||
|
||||
private val chinaAppRegex by lazy {
|
||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||
}
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
|
||||
private var isBlockNotification: Boolean = false
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
context = flutterPluginBinding.applicationContext;
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
private fun initShortcuts(label: String) {
|
||||
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
|
||||
.setShortLabel(label)
|
||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
|
||||
.setIntent(context.getActionIntent("CHANGE"))
|
||||
.build()
|
||||
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
|
||||
}
|
||||
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
scope.cancel()
|
||||
@@ -145,7 +65,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private fun tip(message: String?) {
|
||||
if (GlobalState.flutterEngine == null) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
if (toast != null) {
|
||||
toast!!.cancel()
|
||||
}
|
||||
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||
toast!!.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,29 +77,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
activity?.moveTaskToBack(true)
|
||||
result.success(true)
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
"updateExcludeFromRecents" -> {
|
||||
val value = call.argument<Boolean>("value")
|
||||
updateExcludeFromRecents(value)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"initShortcuts" -> {
|
||||
initShortcuts(call.arguments as String)
|
||||
result.success(true)
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
"getPackages" -> {
|
||||
scope.launch {
|
||||
result.success(getPackagesToJson())
|
||||
}
|
||||
}
|
||||
|
||||
"getChinaPackageNames" -> {
|
||||
scope.launch {
|
||||
result.success(getChinaPackageNames())
|
||||
result.success(getPackages())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +107,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
if (iconMap["default"] == null) {
|
||||
iconMap["default"] =
|
||||
context.packageManager?.defaultActivityIcon?.getBase64()
|
||||
context?.packageManager?.defaultActivityIcon?.getBase64()
|
||||
}
|
||||
result.success(iconMap["default"])
|
||||
return@launch
|
||||
@@ -202,6 +115,52 @@ 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 (context == null) {
|
||||
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)
|
||||
@@ -215,49 +174,52 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFile(path: String) {
|
||||
val file = File(path)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileProvider",
|
||||
file
|
||||
)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
|
||||
uri,
|
||||
"text/plain"
|
||||
)
|
||||
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val resInfoList = context.packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
|
||||
for (resolveInfo in resInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
context.grantUriPermission(
|
||||
packageName,
|
||||
uri,
|
||||
flags
|
||||
context?.let {
|
||||
val file = File(path)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
it,
|
||||
"${it.packageName}.fileProvider",
|
||||
file
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
activity?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
|
||||
uri,
|
||||
"text/plain"
|
||||
)
|
||||
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val resInfoList = it.packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
|
||||
for (resolveInfo in resInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
it.grantUriPermission(
|
||||
packageName,
|
||||
uri,
|
||||
flags
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
activity?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
val am = getSystemService(context, ActivityManager::class.java)
|
||||
if (context == null) return
|
||||
val am = getSystemService(context!!, ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.taskInfo.taskId == activity?.taskId
|
||||
@@ -274,7 +236,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private suspend fun getPackageIcon(packageName: String): String? {
|
||||
val packageManager = context.packageManager
|
||||
val packageManager = context?.packageManager
|
||||
if (iconMap[packageName] == null) {
|
||||
iconMap[packageName] = try {
|
||||
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
||||
@@ -286,145 +248,32 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
return iconMap[packageName]
|
||||
}
|
||||
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = context.packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != context.packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||
firstInstallTime = it.firstInstallTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages
|
||||
}
|
||||
|
||||
private suspend fun getPackagesToJson(): String {
|
||||
private suspend fun getPackages(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
Gson().toJson(getPackages())
|
||||
}
|
||||
}
|
||||
val packageManager = context?.packageManager
|
||||
val packages: List<Package>? =
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != context?.packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
|
||||
private suspend fun getChinaPackageNames(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val packages: List<String> =
|
||||
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1
|
||||
)
|
||||
}
|
||||
Gson().toJson(packages)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestVpnPermission(context: Context, callBack: () -> Unit) {
|
||||
vpnCallBack = callBack
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
|
||||
fun requestNotificationsPermission(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activity == null) return
|
||||
ActivityCompat.requestPermissions(
|
||||
activity!!,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getText(text: String): String? {
|
||||
return runBlocking {
|
||||
channel.awaitResult<String>("getText", text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isChinaPackage(packageName: String): Boolean {
|
||||
val packageManager = context.packageManager ?: return false
|
||||
skipPrefixList.forEach {
|
||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||
}
|
||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
if (packageName.matches(chinaAppRegex)) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") packageManager.getPackageInfo(
|
||||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
mutableListOf<ComponentInfo>().apply {
|
||||
packageInfo.services?.let { addAll(it) }
|
||||
packageInfo.activities?.let { addAll(it) }
|
||||
packageInfo.receivers?.let { addAll(it) }
|
||||
packageInfo.providers?.let { addAll(it) }
|
||||
}.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) return true
|
||||
}
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
fun requestGc() {
|
||||
channel.invokeMethod("gc", null)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
activity = binding.activity;
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
@@ -432,32 +281,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activity = binding.activity;
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
channel.invokeMethod("exit", null)
|
||||
activity = null
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
GlobalState.initServiceEngine(context)
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onRequestPermissionsResultListener(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
isBlockNotification = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var context: Context? = null
|
||||
private var flClashVpnService: FlClashVpnService? = null
|
||||
private var port: Int = 7890
|
||||
private var props: Props? = null
|
||||
private var isBlockNotification: Boolean = false
|
||||
private var isStart: Boolean = false
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as FlClashVpnService.LocalBinder
|
||||
flClashVpnService = binder.getService()
|
||||
if (isStart) {
|
||||
startVpn()
|
||||
} else {
|
||||
flClashVpnService?.initServiceEngine()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
flClashVpnService = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"initService" -> {
|
||||
isStart = false
|
||||
initService()
|
||||
requestNotificationsPermission()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"startProxy" -> {
|
||||
isStart = true
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("args")
|
||||
props =
|
||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||
startVpn()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stopProxy" -> {
|
||||
stopVpn()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
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)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initService() {
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
if (flClashVpnService != null) {
|
||||
flClashVpnService!!.initServiceEngine()
|
||||
} else {
|
||||
bindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpn() {
|
||||
if (flClashVpnService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
stopVpn()
|
||||
return
|
||||
}
|
||||
val fd = flClashVpnService?.start(port, props)
|
||||
flutterMethodChannel.invokeMethod("started", fd)
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashVpnService?.stop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun startForeground(title: String, content: String) {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashVpnService?.startForeground(title, content)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
bindService()
|
||||
} else {
|
||||
stopVpn()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onRequestPermissionsResultListener(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
isBlockNotification = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun requestNotificationsPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = context?.let {
|
||||
ContextCompat.checkSelfPermission(
|
||||
it,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
}
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activity == null) return
|
||||
ActivityCompat.requestPermissions(
|
||||
activity!!,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = Intent(context, FlClashVpnService::class.java)
|
||||
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.content.Context
|
||||
import com.follow.clash.GlobalState
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"init" -> {
|
||||
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context)
|
||||
GlobalState.initServiceEngine(context)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"destroy" -> {
|
||||
handleDestroy()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.getCurrentVPNPlugin()?.stop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
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 androidx.core.content.getSystemService
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.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 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) {
|
||||
flClashService = when (service) {
|
||||
is FlClashVpnService.LocalBinder -> service.getService()
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
else -> throw Exception("invalid binder")
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
|
||||
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" -> {
|
||||
val data = call.argument<String>("data")
|
||||
options = Gson().fromJson(data, VpnOptions::class.java)
|
||||
when (options.enable) {
|
||||
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)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
flClashService?.startForeground(title, content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val fd = flClashService?.start(options)
|
||||
flutterMethodChannel.invokeMethod(
|
||||
"started",
|
||||
fd
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashService?.stop()
|
||||
}
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = when (options.enable) {
|
||||
true -> Intent(context, FlClashVpnService::class.java)
|
||||
false -> Intent(context, FlClashService::class.java)
|
||||
}
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.BaseServiceInterface
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): FlClashService = this@FlClashService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
GlobalState.getText("stop"),
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -37,7 +37,6 @@ 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)
|
||||
@@ -66,7 +65,19 @@ class FlClashTileService : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
activityTransfer()
|
||||
GlobalState.handleToggle(applicationContext)
|
||||
if (GlobalState.runState.value == RunState.STOP) {
|
||||
GlobalState.runState.value = RunState.PENDING
|
||||
val titlePlugin = GlobalState.getCurrentTitlePlugin()
|
||||
if (titlePlugin != null) {
|
||||
titlePlugin.handleStart()
|
||||
} else {
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
} else if (GlobalState.runState.value == RunState.START) {
|
||||
GlobalState.runState.value = RunState.PENDING
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.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
|
||||
@@ -17,58 +15,52 @@ 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.extensions.getActionPendingIntent
|
||||
import com.follow.clash.extensions.getIpv4RouteAddress
|
||||
import com.follow.clash.extensions.getIpv6RouteAddress
|
||||
import com.follow.clash.extensions.toCIDR
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.follow.clash.models.Props
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
class FlClashVpnService : VpnService() {
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
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)
|
||||
initServiceEngine()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions): Int {
|
||||
fun start(port: Int, props: Props?): Int? {
|
||||
return with(Builder()) {
|
||||
if (options.ipv4Address.isNotEmpty()) {
|
||||
val cidr = options.ipv4Address.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv4RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} else {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
}
|
||||
if (options.ipv6Address.isNotEmpty()) {
|
||||
val cidr = options.ipv6Address.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} else {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
addDnsServer(options.dnsServerAddress)
|
||||
addAddress("172.16.0.1", 30)
|
||||
setMtu(9000)
|
||||
options.accessControl?.let { accessControl ->
|
||||
addRoute("0.0.0.0", 0)
|
||||
props?.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
@@ -83,45 +75,33 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
}
|
||||
addDnsServer("172.16.0.2")
|
||||
setSession("FlClash")
|
||||
setBlocking(false)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
setMetered(false)
|
||||
}
|
||||
if (options.allowBypass) {
|
||||
if (props?.allowBypass == true) {
|
||||
allowBypass()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
|
||||
setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
"127.0.0.1",
|
||||
options.port,
|
||||
options.bypassDomain
|
||||
port,
|
||||
passList
|
||||
)
|
||||
)
|
||||
}
|
||||
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() {
|
||||
fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
stopForeground()
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
@@ -140,7 +120,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
@@ -151,45 +130,44 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
GlobalState.getText("stop"),
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||
override fun startForeground(title: String, content: String) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
fun initServiceEngine() {
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentVPNPlugin()?.requestGc()
|
||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForeground() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
@@ -202,7 +180,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
val isSuccess = super.onTransact(code, data, reply, flags)
|
||||
if (!isSuccess) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
}
|
||||
}
|
||||
return isSuccess
|
||||
@@ -212,6 +190,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
64639
assets/data/GeoSite.dat
64639
assets/data/GeoSite.dat
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
Submodule core/Clash.Meta updated: 148f1a2445...3d773d7fa5
474
core/common.go
474
core/common.go
@@ -2,13 +2,20 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"core/state"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/constant/features"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
ap "github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/hub"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/hub/route"
|
||||
"github.com/samber/lo"
|
||||
"github.com/metacubex/mihomo/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -17,37 +24,54 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/batch"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"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/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
rp "github.com/metacubex/mihomo/rules/provider"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
)
|
||||
|
||||
type healthCheckSchema struct {
|
||||
Enable bool `provider:"enable"`
|
||||
URL string `provider:"url"`
|
||||
Interval int `provider:"interval"`
|
||||
TestTimeout int `provider:"timeout,omitempty"`
|
||||
Lazy bool `provider:"lazy,omitempty"`
|
||||
ExpectedStatus string `provider:"expected-status,omitempty"`
|
||||
}
|
||||
|
||||
type proxyProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Path string `provider:"path,omitempty"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Proxy string `provider:"proxy,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
Filter string `provider:"filter,omitempty"`
|
||||
ExcludeFilter string `provider:"exclude-filter,omitempty"`
|
||||
ExcludeType string `provider:"exclude-type,omitempty"`
|
||||
DialerProxy string `provider:"dialer-proxy,omitempty"`
|
||||
|
||||
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
||||
Override ap.OverrideSchema `provider:"override,omitempty"`
|
||||
Header map[string][]string `provider:"header,omitempty"`
|
||||
}
|
||||
|
||||
type ruleProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Behavior string `provider:"behavior"`
|
||||
Path string `provider:"path,omitempty"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Proxy string `provider:"proxy,omitempty"`
|
||||
Format string `provider:"format,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
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 {
|
||||
ProfileId string `json:"profile-id"`
|
||||
Config config.RawConfig `json:"config" `
|
||||
Params ConfigExtendedParams `json:"params"`
|
||||
ProfilePath *string `json:"profile-path"`
|
||||
Config config.RawConfig `json:"config" `
|
||||
Params ConfigExtendedParams `json:"params"`
|
||||
}
|
||||
|
||||
type ChangeProxyParams struct {
|
||||
@@ -66,23 +90,12 @@ type ProcessMapItem struct {
|
||||
}
|
||||
|
||||
type ExternalProvider struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
VehicleType string `json:"vehicle-type"`
|
||||
Count int `json:"count"`
|
||||
Path string `json:"path"`
|
||||
UpdateAt time.Time `json:"update-at"`
|
||||
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
VehicleType string `json:"vehicle-type"`
|
||||
UpdateAt time.Time `json:"update-at"`
|
||||
}
|
||||
|
||||
type ExternalProviders []ExternalProvider
|
||||
|
||||
func (a ExternalProviders) Len() int { return len(a) }
|
||||
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
|
||||
func restartExecutable(execPath string) {
|
||||
var err error
|
||||
executor.Shutdown()
|
||||
@@ -132,158 +145,174 @@ func removeFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getProfilePath(id string) string {
|
||||
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
|
||||
}
|
||||
|
||||
func getProfileProvidersPath(id string) string {
|
||||
return filepath.Join(constant.Path.HomeDir(), "providers", id)
|
||||
}
|
||||
|
||||
func getRawConfigWithId(id string) *config.RawConfig {
|
||||
path := getProfilePath(id)
|
||||
bytes, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Errorln("profile is not exist")
|
||||
func getRawConfigWithPath(path *string) *config.RawConfig {
|
||||
if path == nil {
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
log.Errorln("unmarshalRawConfig error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
for _, mapping := range prof.ProxyProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
} else {
|
||||
bytes, err := readFile(*path)
|
||||
if err != nil {
|
||||
log.Errorln("getProfile readFile error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
log.Errorln("getProfile UnmarshalRawConfig error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
}
|
||||
for _, mapping := range prof.RuleProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
}
|
||||
return prof
|
||||
}
|
||||
|
||||
func getExternalProvidersRaw() map[string]cp.Provider {
|
||||
eps := make(map[string]cp.Provider)
|
||||
for n, p := range tunnel.Providers() {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
eps[n] = p
|
||||
}
|
||||
}
|
||||
for n, p := range tunnel.RuleProviders() {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
eps[n] = p
|
||||
}
|
||||
}
|
||||
return eps
|
||||
}
|
||||
|
||||
func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
|
||||
switch p.(type) {
|
||||
case *provider.ProxySetProvider:
|
||||
psp := p.(*provider.ProxySetProvider)
|
||||
return &ExternalProvider{
|
||||
Name: psp.Name(),
|
||||
Type: psp.Type().String(),
|
||||
VehicleType: psp.VehicleType().String(),
|
||||
Count: psp.Count(),
|
||||
UpdateAt: psp.UpdatedAt(),
|
||||
Path: psp.Vehicle().Path(),
|
||||
SubscriptionInfo: psp.GetSubscriptionInfo(),
|
||||
}, nil
|
||||
case *rp.RuleSetProvider:
|
||||
rsp := p.(*rp.RuleSetProvider)
|
||||
return &ExternalProvider{
|
||||
Name: rsp.Name(),
|
||||
Type: rsp.Type().String(),
|
||||
VehicleType: rsp.VehicleType().String(),
|
||||
Count: rsp.Count(),
|
||||
UpdateAt: rsp.UpdatedAt(),
|
||||
Path: rsp.Vehicle().Path(),
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.New("not external provider")
|
||||
return prof
|
||||
}
|
||||
}
|
||||
|
||||
func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
|
||||
switch p.(type) {
|
||||
case *provider.ProxySetProvider:
|
||||
psp := p.(*provider.ProxySetProvider)
|
||||
_, _, err := psp.SideUpdate(bytes)
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case rp.RuleSetProvider:
|
||||
rsp := p.(*rp.RuleSetProvider)
|
||||
_, _, err := rsp.SideUpdate(bytes)
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("not external provider")
|
||||
}
|
||||
}
|
||||
|
||||
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
|
||||
prof := getRawConfigWithPath(profilePath)
|
||||
overwriteConfig(prof, cfg)
|
||||
return prof
|
||||
}
|
||||
|
||||
func genHosts(hosts, patchHosts map[string]any) {
|
||||
for k, v := range patchHosts {
|
||||
hosts[k] = 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 trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
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
|
||||
return result
|
||||
}
|
||||
|
||||
var ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
|
||||
func replaceFromMap(s string, m map[string]string) string {
|
||||
for k, v := range m {
|
||||
s = strings.ReplaceAll(s, k, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func overrideRules(rules *[]string) {
|
||||
var target = ""
|
||||
for _, line := range *rules {
|
||||
rule := trimArr(strings.Split(line, ","))
|
||||
l := len(rule)
|
||||
if l != 2 {
|
||||
return
|
||||
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{}{}
|
||||
}
|
||||
if strings.ToUpper(rule[0]) == "MATCH" {
|
||||
target = rule[1]
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
return
|
||||
}
|
||||
var rulesExt = lo.Map(ips, func(ip string, index int) string {
|
||||
return fmt.Sprintf("DOMAIN %s %s", ip, target)
|
||||
|
||||
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
|
||||
})
|
||||
*rules = append(rulesExt, *rules...)
|
||||
|
||||
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 overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
|
||||
@@ -293,11 +322,11 @@ 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
|
||||
targetConfig.SocksPort = 0
|
||||
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
|
||||
targetConfig.MixedPort = patchConfig.MixedPort
|
||||
targetConfig.FindProcessMode = patchConfig.FindProcessMode
|
||||
targetConfig.AllowLan = patchConfig.AllowLan
|
||||
@@ -310,93 +339,48 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.Profile.StoreSelected = false
|
||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||
targetConfig.GlobalUA = patchConfig.GlobalUA
|
||||
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 {
|
||||
if targetConfig.DNS.Enable == false {
|
||||
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)
|
||||
//}
|
||||
}
|
||||
|
||||
func patchConfig(general *config.General, controller *config.Controller, tls *config.TLS) {
|
||||
log.Infoln("[Apply] patch")
|
||||
tunnel.SetSniffing(general.Sniffing)
|
||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||
dialer.DefaultInterface.Store(general.Interface)
|
||||
adapter.UnifiedDelay.Store(general.UnifiedDelay)
|
||||
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
|
||||
|
||||
var runLock sync.Mutex
|
||||
|
||||
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
|
||||
if !isRunning {
|
||||
return
|
||||
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)
|
||||
}
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
stopListeners()
|
||||
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
||||
}
|
||||
|
||||
func patchConfig(general *config.General) {
|
||||
log.Infoln("[Apply] patch")
|
||||
route.ReStartServer(general.ExternalController)
|
||||
listener.SetAllowLan(general.AllowLan)
|
||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||
inbound.SetAllowedIPs(general.LanAllowedIPs)
|
||||
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
|
||||
listener.SetBindAddress(general.BindAddress)
|
||||
tunnel.SetSniffing(general.Sniffing)
|
||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||
dialer.DefaultInterface.Store(general.Interface)
|
||||
adapter.UnifiedDelay.Store(general.UnifiedDelay)
|
||||
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.ReCreateTun(general.Tun, 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)
|
||||
if !features.Android {
|
||||
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
|
||||
}
|
||||
}
|
||||
tunnel.SetMode(general.Mode)
|
||||
log.SetLevel(general.LogLevel)
|
||||
|
||||
func stopListeners() {
|
||||
listener.StopListener()
|
||||
resolver.DisableIPv6 = !general.IPv6
|
||||
}
|
||||
|
||||
func patchSelectGroup() {
|
||||
@@ -424,19 +408,25 @@ func patchSelectGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
var applyLock sync.Mutex
|
||||
|
||||
func applyConfig() error {
|
||||
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
|
||||
applyLock.Lock()
|
||||
defer applyLock.Unlock()
|
||||
cfg, err := config.ParseRawConfig(currentConfig)
|
||||
if err != nil {
|
||||
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
if configParams.TestURL != nil {
|
||||
constant.DefaultTestURL = *configParams.TestURL
|
||||
}
|
||||
if configParams.IsPatch {
|
||||
patchConfig(cfg.General, cfg.Controller, cfg.TLS)
|
||||
patchConfig(cfg.General)
|
||||
} else {
|
||||
closeConnections()
|
||||
runtime.GC()
|
||||
hub.ApplyConfig(cfg)
|
||||
hub.UltraApplyConfig(cfg, true)
|
||||
patchSelectGroup()
|
||||
}
|
||||
updateListeners(cfg.General, cfg.Listeners)
|
||||
return err
|
||||
}
|
||||
|
||||
20
core/dns.go
20
core/dns.go
@@ -1,20 +0,0 @@
|
||||
//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()
|
||||
}()
|
||||
}
|
||||
64
core/go.mod
64
core/go.mod
@@ -4,18 +4,14 @@ go 1.21.0
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require github.com/metacubex/mihomo v1.17.1
|
||||
|
||||
require (
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/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
|
||||
)
|
||||
|
||||
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
|
||||
@@ -24,28 +20,30 @@ 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.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/coreos/go-iptables v0.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.14 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
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.3.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
@@ -54,21 +52,19 @@ 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.47.1-0.20240909010619-6b38f24bfcc4 // indirect
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-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-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-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // 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
|
||||
@@ -76,9 +72,10 @@ 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.4.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
|
||||
github.com/sagernet/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
|
||||
@@ -86,7 +83,8 @@ 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/samber/lo v1.47.0
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
|
||||
github.com/samber/lo v1.39.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
@@ -103,16 +101,14 @@ require (
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.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/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/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
139
core/go.sum
139
core/go.sum
@@ -1,5 +1,7 @@
|
||||
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=
|
||||
@@ -17,15 +19,17 @@ 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.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
|
||||
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/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=
|
||||
@@ -36,12 +40,16 @@ 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.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
|
||||
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
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=
|
||||
@@ -57,8 +65,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.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
|
||||
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@@ -74,8 +82,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-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 h1:dh8D8FksyMhD64mRMbUhZHWYJfNoNMCxfVq6eexleMw=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
@@ -84,6 +92,10 @@ 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=
|
||||
@@ -94,42 +106,34 @@ 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.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/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvWDcBDAkIv5kUYIhzHwafDVq635BuybnKqI=
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing 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-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-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
|
||||
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
|
||||
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||
github.com/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/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||
@@ -152,28 +156,35 @@ 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.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
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/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/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/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/shirou/gopsutil/v3 v3.24.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=
|
||||
@@ -189,15 +200,9 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
@@ -211,10 +216,6 @@ 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=
|
||||
@@ -229,21 +230,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.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/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.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=
|
||||
@@ -259,19 +260,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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
|
||||
471
core/hub.go
471
core/hub.go
@@ -1 +1,472 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/structure"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
rp "github.com/metacubex/mihomo/rules/provider"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||
"golang.org/x/net/context"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var currentConfig = config.DefaultRawConfig()
|
||||
|
||||
var configParams = ConfigExtendedParams{}
|
||||
|
||||
var isInit = false
|
||||
|
||||
var currentProfileName = ""
|
||||
|
||||
//export initClash
|
||||
func initClash(homeDirStr *C.char) bool {
|
||||
if !isInit {
|
||||
constant.SetHomeDir(C.GoString(homeDirStr))
|
||||
isInit = true
|
||||
}
|
||||
return isInit
|
||||
}
|
||||
|
||||
//export getIsInit
|
||||
func getIsInit() bool {
|
||||
return isInit
|
||||
}
|
||||
|
||||
//export restartClash
|
||||
func restartClash() bool {
|
||||
execPath, _ := os.Executable()
|
||||
go restartExecutable(execPath)
|
||||
return true
|
||||
}
|
||||
|
||||
//export shutdownClash
|
||||
func shutdownClash() bool {
|
||||
executor.Shutdown()
|
||||
runtime.GC()
|
||||
isInit = false
|
||||
currentConfig = nil
|
||||
return true
|
||||
}
|
||||
|
||||
//export forceGc
|
||||
func forceGc() {
|
||||
go func() {
|
||||
log.Infoln("[APP] request force GC")
|
||||
runtime.GC()
|
||||
}()
|
||||
}
|
||||
|
||||
//export setCurrentProfileName
|
||||
func setCurrentProfileName(s *C.char) {
|
||||
currentProfileName = C.GoString(s)
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(currentProfileName)
|
||||
}
|
||||
|
||||
//export validateConfig
|
||||
func validateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
_, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export updateConfig
|
||||
func updateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
var params = &GenerateConfigParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
configParams = params.Params
|
||||
prof := decorationConfig(params.ProfilePath, params.Config)
|
||||
currentConfig = prof
|
||||
err = applyConfig()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export clearEffect
|
||||
func clearEffect(s *C.char) {
|
||||
path := C.GoString(s)
|
||||
go func() {
|
||||
rawCfg := getRawConfigWithPath(&path)
|
||||
for _, mapping := range rawCfg.RuleProvider {
|
||||
schema := &ruleProviderSchema{}
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||
if err := decoder.Decode(mapping, schema); err != nil {
|
||||
return
|
||||
}
|
||||
if schema.Type == "http" {
|
||||
_ = removeFile(constant.Path.Resolve(schema.Path))
|
||||
}
|
||||
}
|
||||
for _, mapping := range rawCfg.ProxyProvider {
|
||||
schema := &proxyProviderSchema{
|
||||
HealthCheck: healthCheckSchema{
|
||||
Lazy: true,
|
||||
},
|
||||
}
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||
if err := decoder.Decode(mapping, schema); err != nil {
|
||||
return
|
||||
}
|
||||
if schema.Type == "http" {
|
||||
_ = removeFile(constant.Path.Resolve(schema.Path))
|
||||
}
|
||||
}
|
||||
_ = removeFile(path)
|
||||
}()
|
||||
}
|
||||
|
||||
//export getProxies
|
||||
func getProxies() *C.char {
|
||||
data, err := json.Marshal(tunnel.ProxiesWithProviders())
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export changeProxy
|
||||
func changeProxy(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
var params = &ChangeProxyParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
log.Infoln("Unmarshal ChangeProxyParams %v", err)
|
||||
}
|
||||
groupName := *params.GroupName
|
||||
proxyName := *params.ProxyName
|
||||
proxies := tunnel.ProxiesWithProviders()
|
||||
group, ok := proxies[groupName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
adapterProxy := group.(*adapter.Proxy)
|
||||
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err = selector.Set(proxyName)
|
||||
if err == nil {
|
||||
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
|
||||
}
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Current(state.OnlyProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
}
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Total(state.OnlyProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
}
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export resetTraffic
|
||||
func resetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
|
||||
//export asyncTestDelay
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
var params = &TestDelayParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
|
||||
defer cancel()
|
||||
|
||||
proxies := tunnel.ProxiesWithProviders()
|
||||
proxy := proxies[params.ProxyName]
|
||||
|
||||
delayData := &Delay{
|
||||
Name: params.ProxyName,
|
||||
}
|
||||
|
||||
if proxy == nil {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
}
|
||||
|
||||
delayData.Value = int32(delay)
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
}()
|
||||
}
|
||||
|
||||
//export getVersionInfo
|
||||
func getVersionInfo() *C.char {
|
||||
versionInfo := map[string]string{
|
||||
"clashName": constant.Name,
|
||||
"version": "1.18.5",
|
||||
}
|
||||
data, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getConnections
|
||||
func getConnections() *C.char {
|
||||
snapshot := statistic.DefaultManager.Snapshot()
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export closeConnections
|
||||
func closeConnections() {
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
//export closeConnection
|
||||
func closeConnection(id *C.char) {
|
||||
connectionId := C.GoString(id)
|
||||
c := statistic.DefaultManager.Get(connectionId)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
_ = c.Close()
|
||||
}
|
||||
|
||||
//export getProviders
|
||||
func getProviders() *C.char {
|
||||
data, err := json.Marshal(tunnel.Providers())
|
||||
var msg *C.char
|
||||
if err != nil {
|
||||
msg = C.CString("")
|
||||
return msg
|
||||
}
|
||||
msg = C.CString(string(data))
|
||||
return msg
|
||||
}
|
||||
|
||||
//export getProvider
|
||||
func getProvider(name *C.char) *C.char {
|
||||
providerName := C.GoString(name)
|
||||
providers := tunnel.Providers()
|
||||
data, err := json.Marshal(providers[providerName])
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getExternalProviders
|
||||
func getExternalProviders() *C.char {
|
||||
externalProviders := make([]ExternalProvider, 0)
|
||||
providers := tunnel.Providers()
|
||||
for n, p := range providers {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
p := p.(*provider.ProxySetProvider)
|
||||
externalProviders = append(externalProviders, ExternalProvider{
|
||||
Name: n,
|
||||
Type: p.Type().String(),
|
||||
VehicleType: p.VehicleType().String(),
|
||||
UpdateAt: p.UpdatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
for n, p := range tunnel.RuleProviders() {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
p := p.(*rp.RuleSetProvider)
|
||||
externalProviders = append(externalProviders, ExternalProvider{
|
||||
Name: n,
|
||||
Type: p.Type().String(),
|
||||
VehicleType: p.VehicleType().String(),
|
||||
UpdateAt: p.UpdatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(externalProviders)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export updateExternalProvider
|
||||
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
providerNameString := C.GoString(providerName)
|
||||
providerTypeString := C.GoString(providerType)
|
||||
go func() {
|
||||
switch providerTypeString {
|
||||
case "Proxy":
|
||||
providers := tunnel.Providers()
|
||||
err := providers[providerNameString].Update()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "Rule":
|
||||
providers := tunnel.RuleProviders()
|
||||
err := providers[providerNameString].Update()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "MMDB":
|
||||
err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "ASN":
|
||||
err := updater.UpdateASN(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export initNativeApiBridge
|
||||
func initNativeApiBridge(api unsafe.Pointer) {
|
||||
bridge.InitDartApi(api)
|
||||
}
|
||||
|
||||
//export initMessage
|
||||
func initMessage(port C.longlong) {
|
||||
i := int64(port)
|
||||
Port = i
|
||||
}
|
||||
|
||||
//export freeCString
|
||||
func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func init() {
|
||||
provider.HealthcheckHook = func(name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
Name: name,
|
||||
}
|
||||
if delay == 0 {
|
||||
delayData.Value = -1
|
||||
} else {
|
||||
delayData.Value = int32(delay)
|
||||
}
|
||||
SendMessage(Message{
|
||||
Type: DelayMessage,
|
||||
Data: delayData,
|
||||
})
|
||||
}
|
||||
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
||||
SendMessage(Message{
|
||||
Type: RequestMessage,
|
||||
Data: c,
|
||||
})
|
||||
}
|
||||
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
|
||||
SendMessage(Message{
|
||||
Type: LoadedMessage,
|
||||
Data: providerName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
475
core/lib.go
475
core/lib.go
@@ -1,475 +0,0 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
bridge "core/dart-bridge"
|
||||
"core/state"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||
)
|
||||
|
||||
var configParams = ConfigExtendedParams{}
|
||||
|
||||
var externalProviders = map[string]cp.Provider{}
|
||||
|
||||
var isInit = false
|
||||
|
||||
//export start
|
||||
func start() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = true
|
||||
}
|
||||
|
||||
//export stop
|
||||
func stop() {
|
||||
runLock.Lock()
|
||||
go func() {
|
||||
defer runLock.Unlock()
|
||||
isRunning = false
|
||||
stopListeners()
|
||||
}()
|
||||
}
|
||||
|
||||
//export initClash
|
||||
func initClash(homeDirStr *C.char) bool {
|
||||
if !isInit {
|
||||
constant.SetHomeDir(C.GoString(homeDirStr))
|
||||
isInit = true
|
||||
}
|
||||
return isInit
|
||||
}
|
||||
|
||||
//export getIsInit
|
||||
func getIsInit() bool {
|
||||
return isInit
|
||||
}
|
||||
|
||||
//export restartClash
|
||||
func restartClash() bool {
|
||||
execPath, _ := os.Executable()
|
||||
go restartExecutable(execPath)
|
||||
return true
|
||||
}
|
||||
|
||||
//export shutdownClash
|
||||
func shutdownClash() bool {
|
||||
stopListeners()
|
||||
executor.Shutdown()
|
||||
runtime.GC()
|
||||
isInit = false
|
||||
return true
|
||||
}
|
||||
|
||||
//export forceGc
|
||||
func forceGc() {
|
||||
go func() {
|
||||
log.Infoln("[APP] request force GC")
|
||||
runtime.GC()
|
||||
}()
|
||||
}
|
||||
|
||||
//export validateConfig
|
||||
func validateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
_, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
var updateLock sync.Mutex
|
||||
|
||||
//export updateConfig
|
||||
func updateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
updateLock.Lock()
|
||||
defer updateLock.Unlock()
|
||||
var params = &GenerateConfigParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
configParams = params.Params
|
||||
prof := decorationConfig(params.ProfileId, params.Config)
|
||||
state.CurrentRawConfig = prof
|
||||
err = applyConfig()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export clearEffect
|
||||
func clearEffect(s *C.char) {
|
||||
id := C.GoString(s)
|
||||
go func() {
|
||||
_ = removeFile(getProfilePath(id))
|
||||
_ = removeFile(getProfileProvidersPath(id))
|
||||
}()
|
||||
}
|
||||
|
||||
//export getProxies
|
||||
func getProxies() *C.char {
|
||||
data, err := json.Marshal(tunnel.ProxiesWithProviders())
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export changeProxy
|
||||
func changeProxy(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
var params = &ChangeProxyParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
log.Infoln("Unmarshal ChangeProxyParams %v", err)
|
||||
}
|
||||
groupName := *params.GroupName
|
||||
proxyName := *params.ProxyName
|
||||
proxies := tunnel.ProxiesWithProviders()
|
||||
group, ok := proxies[groupName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
adapterProxy := group.(*adapter.Proxy)
|
||||
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if proxyName == "" {
|
||||
selector.ForceSet(proxyName)
|
||||
} else {
|
||||
err = selector.Set(proxyName)
|
||||
}
|
||||
if err == nil {
|
||||
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
|
||||
}
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
}
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
}
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export resetTraffic
|
||||
func resetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
|
||||
//export asyncTestDelay
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
b.Go(paramsString, func() (bool, error) {
|
||||
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
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
|
||||
defer cancel()
|
||||
|
||||
proxies := tunnel.ProxiesWithProviders()
|
||||
proxy := proxies[params.ProxyName]
|
||||
|
||||
delayData := &Delay{
|
||||
Name: params.ProxyName,
|
||||
}
|
||||
|
||||
if proxy == nil {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delayData.Value = int32(delay)
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
//export getVersionInfo
|
||||
func getVersionInfo() *C.char {
|
||||
versionInfo := map[string]string{
|
||||
"clashName": constant.Name,
|
||||
"version": "1.18.5",
|
||||
}
|
||||
data, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getConnections
|
||||
func getConnections() *C.char {
|
||||
snapshot := statistic.DefaultManager.Snapshot()
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export closeConnections
|
||||
func closeConnections() {
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
//export closeConnection
|
||||
func closeConnection(id *C.char) {
|
||||
connectionId := C.GoString(id)
|
||||
c := statistic.DefaultManager.Get(connectionId)
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
_ = c.Close()
|
||||
}
|
||||
|
||||
//export getExternalProviders
|
||||
func getExternalProviders() *C.char {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
externalProviders = getExternalProvidersRaw()
|
||||
eps := make([]ExternalProvider, 0)
|
||||
for _, p := range externalProviders {
|
||||
externalProvider, err := toExternalProvider(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
eps = append(eps, *externalProvider)
|
||||
}
|
||||
sort.Sort(ExternalProviders(eps))
|
||||
data, err := json.Marshal(eps)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getExternalProvider
|
||||
func getExternalProvider(name *C.char) *C.char {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
externalProviderName := C.GoString(name)
|
||||
externalProvider, exist := externalProviders[externalProviderName]
|
||||
if !exist {
|
||||
return C.CString("")
|
||||
}
|
||||
e, err := toExternalProvider(externalProvider)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
data, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export updateGeoData
|
||||
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
geoTypeString := C.GoString(geoType)
|
||||
geoNameString := C.GoString(geoName)
|
||||
go func() {
|
||||
path := constant.Path.Resolve(geoNameString)
|
||||
switch geoTypeString {
|
||||
case "MMDB":
|
||||
err := updater.UpdateMMDBWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "ASN":
|
||||
err := updater.UpdateASNWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
err := updater.UpdateGeoIpWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := updater.UpdateGeoSiteWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export updateExternalProvider
|
||||
func updateExternalProvider(providerName *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
providerNameString := C.GoString(providerName)
|
||||
go func() {
|
||||
externalProvider, exist := externalProviders[providerNameString]
|
||||
if !exist {
|
||||
bridge.SendToPort(i, "external provider is not exist")
|
||||
return
|
||||
}
|
||||
err := externalProvider.Update()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export sideLoadExternalProvider
|
||||
func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(data))
|
||||
providerNameString := C.GoString(providerName)
|
||||
go func() {
|
||||
externalProvider, exist := externalProviders[providerNameString]
|
||||
if !exist {
|
||||
bridge.SendToPort(i, "external provider is not exist")
|
||||
return
|
||||
}
|
||||
err := sideUpdateExternalProvider(externalProvider, bytes)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export initNativeApiBridge
|
||||
func initNativeApiBridge(api unsafe.Pointer) {
|
||||
bridge.InitDartApi(api)
|
||||
}
|
||||
|
||||
//export initMessage
|
||||
func initMessage(port C.longlong) {
|
||||
i := int64(port)
|
||||
Port = i
|
||||
}
|
||||
|
||||
//export freeCString
|
||||
func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func init() {
|
||||
provider.HealthcheckHook = func(name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
Name: name,
|
||||
}
|
||||
if delay == 0 {
|
||||
delayData.Value = -1
|
||||
} else {
|
||||
delayData.Value = int32(delay)
|
||||
}
|
||||
SendMessage(Message{
|
||||
Type: DelayMessage,
|
||||
Data: delayData,
|
||||
})
|
||||
}
|
||||
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
||||
SendMessage(Message{
|
||||
Type: RequestMessage,
|
||||
Data: c,
|
||||
})
|
||||
}
|
||||
executor.DefaultProviderLoadedHook = func(providerName string) {
|
||||
SendMessage(Message{
|
||||
Type: LoadedMessage,
|
||||
Data: providerName,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,34 @@ package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"core/state"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
if state.CurrentState == nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(state.CurrentState.CurrentProfileName)
|
||||
type AccessControl struct {
|
||||
Mode string `json:"mode"`
|
||||
AcceptList []string `json:"acceptList"`
|
||||
RejectList []string `json:"rejectList"`
|
||||
IsFilterSystemApp bool `json:"isFilterSystemApp"`
|
||||
}
|
||||
|
||||
//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,
|
||||
RouteAddress: state.CurrentState.RouteAddress,
|
||||
BypassDomain: state.CurrentState.BypassDomain,
|
||||
DnsServerAddress: state.GetDnsServerAddress(),
|
||||
}
|
||||
data, err := json.Marshal(options)
|
||||
type AndroidProps struct {
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
AndroidProps
|
||||
MixedPort int `json:"mixedPort"`
|
||||
OnlyProxy bool `json:"onlyProxy"`
|
||||
}
|
||||
|
||||
var state State
|
||||
|
||||
//export getState
|
||||
func getState() *C.char {
|
||||
data, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
@@ -40,7 +40,7 @@ func getAndroidVpnOptions() *C.char {
|
||||
//export setState
|
||||
func setState(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
||||
err := json.Unmarshal([]byte(paramsString), &state)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
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"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
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
|
||||
}
|
||||
74
core/tun.go
74
core/tun.go
@@ -7,18 +7,18 @@ import (
|
||||
"core/platform"
|
||||
t "core/tun"
|
||||
"errors"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
var tunLock sync.Mutex
|
||||
var tun *t.Tun
|
||||
var runTime *time.Time
|
||||
|
||||
type FdMap struct {
|
||||
@@ -34,37 +34,39 @@ func (cm *FdMap) Load(key int64) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
var (
|
||||
tunListener *sing_tun.Listener
|
||||
fdMap FdMap
|
||||
fdCounter int64 = 0
|
||||
)
|
||||
var fdMap FdMap
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int, port C.longlong) {
|
||||
i := int64(port)
|
||||
ServicePort = i
|
||||
if fd == 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
SendMessage(Message{
|
||||
Type: StartedMessage,
|
||||
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
||||
})
|
||||
return
|
||||
}
|
||||
initSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
f := int(fd)
|
||||
tunListener, _ = t.Start(f)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
tempTun.Closer = closer
|
||||
|
||||
tun = tempTun
|
||||
|
||||
now := time.Now()
|
||||
|
||||
runTime = &now
|
||||
@@ -86,15 +88,15 @@ func getRunTime() *C.char {
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
removeSocketHook()
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
|
||||
runTime = nil
|
||||
|
||||
if tunListener != nil {
|
||||
_ = tunListener.Close()
|
||||
if tun != nil {
|
||||
tun.Close()
|
||||
tun = nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -121,14 +123,20 @@ func markSocket(fd Fd) {
|
||||
})
|
||||
}
|
||||
|
||||
func initSocketHook() {
|
||||
var fdCounter int64 = 0
|
||||
|
||||
func init() {
|
||||
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(500 * time.Millisecond)
|
||||
timeout := time.After(100 * time.Millisecond)
|
||||
id := atomic.AddInt64(&fdCounter, 1)
|
||||
|
||||
markSocket(Fd{
|
||||
@@ -145,13 +153,9 @@ func initSocketHook() {
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func removeSocketHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
}
|
||||
|
||||
33
core/tun/dns.go
Normal file
33
core/tun/dns.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//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()
|
||||
}
|
||||
20
core/tun/tcp.go
Normal file
20
core/tun/tcp.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//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: "",
|
||||
}
|
||||
}
|
||||
206
core/tun/tun.go
206
core/tun/tun.go
@@ -4,65 +4,183 @@ package tun
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"core/state"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"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"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"`
|
||||
type Tun struct {
|
||||
Closer io.Closer
|
||||
|
||||
Closed bool
|
||||
Limit *semaphore.Weighted
|
||||
}
|
||||
|
||||
func Start(fd int) (*sing_tun.Listener, error) {
|
||||
var prefix4 []netip.Prefix
|
||||
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
panic(err.Error())
|
||||
} else {
|
||||
network.IP = ip
|
||||
}
|
||||
stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal))
|
||||
|
||||
if err != nil {
|
||||
_ = device.Close()
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
|
||||
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))
|
||||
}
|
||||
prefix6 = append(prefix6, tempPrefix6)
|
||||
}
|
||||
|
||||
var dnsHijack []string
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53"))
|
||||
udp := func() {
|
||||
defer func(udp *nat.UDP) {
|
||||
_ = udp.Close()
|
||||
}(stack.UDP())
|
||||
defer log.Debugln("UDP: closed")
|
||||
|
||||
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,
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := sing_tun.New(options, tunnel.Tunnel)
|
||||
go tcp()
|
||||
go udp()
|
||||
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listener, nil
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
28
core/tun/udp.go
Normal file
28
core/tun/udp.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:animations/animations.dart';
|
||||
|
||||
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/plugins/app.dart';
|
||||
import 'package:fl_clash/state.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';
|
||||
@@ -19,7 +17,6 @@ runAppWithPreferences(
|
||||
Widget child, {
|
||||
required AppState appState,
|
||||
required Config config,
|
||||
required AppFlowingState appFlowingState,
|
||||
required ClashConfig clashConfig,
|
||||
}) {
|
||||
runApp(MultiProvider(
|
||||
@@ -30,13 +27,11 @@ 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!;
|
||||
},
|
||||
@@ -57,22 +52,13 @@ class Application extends StatefulWidget {
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
Timer? timer;
|
||||
|
||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.windows: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.linux: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.macOS: SharedAxisPageTransitionsBuilder(
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
),
|
||||
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -94,9 +80,7 @@ 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) {
|
||||
@@ -104,56 +88,25 @@ class ApplicationState extends State<Application> {
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
app?.initShortcuts();
|
||||
_updateGroups();
|
||||
});
|
||||
}
|
||||
|
||||
_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 WindowManager(
|
||||
child: TrayManager(
|
||||
child: HotKeyManager(
|
||||
child: ProxyManager(
|
||||
child: app,
|
||||
),
|
||||
),
|
||||
return WindowContainer(
|
||||
child: TrayContainer(
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
}
|
||||
return AndroidManager(
|
||||
child: TileManager(
|
||||
return AndroidContainer(
|
||||
child: TileContainer(
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildPage(Widget page) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: page,
|
||||
);
|
||||
}
|
||||
|
||||
_updateSystemColorSchemes(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
@@ -167,76 +120,75 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_updateGroups() {
|
||||
if (globalState.groupsUpdateTimer != null) {
|
||||
globalState.groupsUpdateTimer?.cancel();
|
||||
globalState.groupsUpdateTimer = null;
|
||||
}
|
||||
globalState.groupsUpdateTimer ??= Timer.periodic(
|
||||
httpTimeoutDuration,
|
||||
(timer) async {
|
||||
await globalState.appController.updateGroupDebounce();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildApp(
|
||||
AppStateManager(
|
||||
child: ClashManager(
|
||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
themeMode: config.themeProps.themeMode,
|
||||
primaryColor: config.themeProps.primaryColor,
|
||||
prueBlack: config.themeProps.prueBlack,
|
||||
fontFamily: config.themeProps.fontFamily,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return LayoutBuilder(
|
||||
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,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
supportedLocales:
|
||||
AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
return AppStateContainer(
|
||||
child: ClashContainer(
|
||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.locale,
|
||||
themeMode: config.themeMode,
|
||||
primaryColor: config.primaryColor,
|
||||
prueBlack: config.prueBlack,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return _buildApp(child!);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -247,6 +199,5 @@ class ApplicationState extends State<Application> {
|
||||
linkManager.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
super.dispose();
|
||||
_cancelTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
|
||||
import 'generated/clash_ffi.dart';
|
||||
|
||||
class ClashCore {
|
||||
@@ -101,6 +100,22 @@ class ClashCore {
|
||||
);
|
||||
}
|
||||
|
||||
setProfileName(String profileName) {
|
||||
final profileNameChar = profileName.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setCurrentProfileName(
|
||||
profileNameChar,
|
||||
);
|
||||
malloc.free(profileNameChar);
|
||||
}
|
||||
|
||||
getProfileName() {
|
||||
final currentProfileNameRaw = clashFFI.getCurrentProfileName();
|
||||
final currentProfileName =
|
||||
currentProfileNameRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(currentProfileNameRaw);
|
||||
return currentProfileName;
|
||||
}
|
||||
|
||||
Future<List<Group>> getProxiesGroups() {
|
||||
final proxiesRaw = clashFFI.getProxies();
|
||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
||||
@@ -150,69 +165,9 @@ class ClashCore {
|
||||
});
|
||||
}
|
||||
|
||||
ExternalProvider? getExternalProvider(String externalProviderName) {
|
||||
final externalProviderNameChar =
|
||||
externalProviderName.toNativeUtf8().cast<Char>();
|
||||
final externalProviderRaw =
|
||||
clashFFI.getExternalProvider(externalProviderNameChar);
|
||||
malloc.free(externalProviderNameChar);
|
||||
final externalProviderRawString =
|
||||
externalProviderRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(externalProviderRaw);
|
||||
if (externalProviderRawString.isEmpty) return null;
|
||||
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
|
||||
}
|
||||
|
||||
Future<String> updateGeoData({
|
||||
required String geoType,
|
||||
required String geoName,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
|
||||
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateGeoData(
|
||||
geoTypeChar,
|
||||
geoNameChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(geoTypeChar);
|
||||
malloc.free(geoNameChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||
clashFFI.sideLoadExternalProvider(
|
||||
providerNameChar,
|
||||
dataChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
malloc.free(dataChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<String> updateExternalProvider({
|
||||
required String providerName,
|
||||
required String providerType,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
@@ -223,11 +178,14 @@ class ClashCore {
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
final providerTypeChar = providerType.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateExternalProvider(
|
||||
providerNameChar,
|
||||
providerTypeChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
malloc.free(providerTypeChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@@ -238,14 +196,6 @@ class ClashCore {
|
||||
malloc.free(paramsChar);
|
||||
}
|
||||
|
||||
start() {
|
||||
clashFFI.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
clashFFI.stop();
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
@@ -266,13 +216,21 @@ class ClashCore {
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(delayParamsChar);
|
||||
Future.delayed(httpTimeoutDuration + moreDuration, () {
|
||||
receiver.close();
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(
|
||||
Delay(name: proxyName, value: -1),
|
||||
);
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
clearEffect(String profileId) {
|
||||
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
|
||||
clashFFI.clearEffect(profileIdChar);
|
||||
malloc.free(profileIdChar);
|
||||
clearEffect(String path) {
|
||||
final pathChar = path.toNativeUtf8().cast<Char>();
|
||||
clashFFI.clearEffect(pathChar);
|
||||
malloc.free(pathChar);
|
||||
}
|
||||
|
||||
VersionInfo getVersionInfo() {
|
||||
@@ -288,18 +246,13 @@ class ClashCore {
|
||||
malloc.free(stateChar);
|
||||
}
|
||||
|
||||
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);
|
||||
CoreState getState() {
|
||||
final stateRaw = clashFFI.getState();
|
||||
final state = json.decode(
|
||||
stateRaw.cast<Utf8>().toDartString(),
|
||||
);
|
||||
clashFFI.freeCString(stateRaw);
|
||||
return CoreState.fromJson(state);
|
||||
}
|
||||
|
||||
Traffic getTraffic() {
|
||||
@@ -333,13 +286,6 @@ 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();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class Android {
|
||||
init() async {
|
||||
app?.onExit = () async {
|
||||
await globalState.appController.savePreferences();
|
||||
};
|
||||
app?.onExit = () {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
extension ArchiveExt on Archive {
|
||||
addDirectoryToArchive(String dirPath, String parentPath) {
|
||||
final dir = Directory(dirPath);
|
||||
final entities = dir.listSync(recursive: false);
|
||||
for (final entity in entities) {
|
||||
final relativePath = relative(entity.path, from: parentPath);
|
||||
if (entity is File) {
|
||||
final data = entity.readAsBytesSync();
|
||||
final archiveFile = ArchiveFile(relativePath, data.length, data);
|
||||
addFile(archiveFile);
|
||||
} else if (entity is Directory) {
|
||||
addDirectoryToArchive(entity.path, parentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add<T>(String name, T raw) {
|
||||
final data = json.encode(raw);
|
||||
addFile(
|
||||
ArchiveFile(name, data.length, data),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,13 @@ extension ColorExtension on Color {
|
||||
toLittle() {
|
||||
return withOpacity(0.03);
|
||||
}
|
||||
|
||||
Color darken([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorSchemeExtension on ColorScheme {
|
||||
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(0.05),
|
||||
background: Colors.black,
|
||||
)
|
||||
: this;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@ export 'app_localizations.dart';
|
||||
export 'function.dart';
|
||||
export 'package.dart';
|
||||
export 'measure.dart';
|
||||
export 'windows.dart';
|
||||
export 'service.dart';
|
||||
export 'iterable.dart';
|
||||
export 'scroll.dart';
|
||||
export 'icons.dart';
|
||||
export 'http.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'network.dart';
|
||||
export 'navigator.dart';
|
||||
export 'scroll.dart';
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/models/clash_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'system.dart';
|
||||
|
||||
@@ -18,18 +17,14 @@ const mmdbFileName = "geoip.metadb";
|
||||
const asnFileName = "ASN.mmdb";
|
||||
const geoIpFileName = "GeoIP.dat";
|
||||
const geoSiteFileName = "GeoSite.dat";
|
||||
final double kHeaderHeight = system.isDesktop
|
||||
? !Platform.isMacOS
|
||||
? 40
|
||||
: 26
|
||||
: 0;
|
||||
final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0;
|
||||
const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
"asn":
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"https://github.com/xishang0128/geoip/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"
|
||||
};
|
||||
@@ -52,21 +47,6 @@ 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],
|
||||
|
||||
@@ -11,7 +11,7 @@ extension BuildContextExtension on BuildContext {
|
||||
return MediaQuery.of(this).size;
|
||||
}
|
||||
|
||||
double get viewWidth {
|
||||
double get width {
|
||||
return appSize.width;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
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:path/path.dart';
|
||||
import 'package:webdav_client/webdav_client.dart';
|
||||
|
||||
class DAVClient {
|
||||
late Client client;
|
||||
Completer<bool> pingCompleter = Completer();
|
||||
late String fileName;
|
||||
|
||||
DAVClient(DAV dav) {
|
||||
client = newClient(
|
||||
@@ -16,7 +18,6 @@ class DAVClient {
|
||||
user: dav.user,
|
||||
password: dav.password,
|
||||
);
|
||||
fileName = dav.fileName;
|
||||
client.setHeaders(
|
||||
{
|
||||
'accept-charset': 'utf-8',
|
||||
@@ -32,6 +33,8 @@ class DAVClient {
|
||||
Future<bool> _ping() async {
|
||||
try {
|
||||
await client.ping();
|
||||
await client.mkdir("/$appName");
|
||||
await client.mkdir("/$appName/$profilesDirectoryName");
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
@@ -40,17 +43,65 @@ class DAVClient {
|
||||
|
||||
get root => "/$appName";
|
||||
|
||||
get backupFile => "$root/$fileName";
|
||||
get remoteConfig => "$root/$configKey.json";
|
||||
|
||||
backup(Uint8List data) async {
|
||||
get remoteClashConfig => "$root/$clashConfigKey.json";
|
||||
|
||||
get remoteProfiles => "$root/$profilesDirectoryName";
|
||||
|
||||
backup() async {
|
||||
final appController = globalState.appController;
|
||||
final config = appController.config;
|
||||
final clashConfig = appController.clashConfig;
|
||||
await client.mkdir("$root");
|
||||
await client.write("$backupFile", data);
|
||||
client.write(
|
||||
remoteConfig,
|
||||
utf8.encode(
|
||||
json.encode(config.toJson()),
|
||||
),
|
||||
);
|
||||
client.write(
|
||||
remoteClashConfig,
|
||||
utf8.encode(
|
||||
json.encode(clashConfig.toJson()),
|
||||
),
|
||||
);
|
||||
await client.remove(remoteProfiles);
|
||||
for (final profile in config.profiles) {
|
||||
final path = await appPath.getProfilePath(profile.id);
|
||||
if (path == null) continue;
|
||||
await client.writeFromFile(
|
||||
path,
|
||||
"$remoteProfiles/${basename(path)}",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<List<int>> recovery() async {
|
||||
await client.mkdir("$root");
|
||||
final data = await client.read(backupFile);
|
||||
return data;
|
||||
recovery({required RecoveryOption recoveryOption}) async {
|
||||
final profiles = await client.readDir(remoteProfiles);
|
||||
final profilesPath = await appPath.getProfilesPath();
|
||||
for (final file in profiles) {
|
||||
await client.read2File(
|
||||
"$remoteProfiles/${file.name}",
|
||||
join(
|
||||
profilesPath,
|
||||
file.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
final configRaw = utf8.decode((await client.read(remoteConfig)));
|
||||
final clashConfigRaw = utf8.decode(await client.read(remoteClashConfig));
|
||||
final config = Config.fromJson(json.decode(configRaw));
|
||||
final clashConfig = ClashConfig.fromJson(json.decode(clashConfigRaw));
|
||||
if(recoveryOption == RecoveryOption.onlyProfiles){
|
||||
globalState.appController.config.update(config, RecoveryOption.onlyProfiles);
|
||||
}else{
|
||||
globalState.appController.config.update(config, RecoveryOption.all);
|
||||
globalState.appController.clashConfig.update(clashConfig);
|
||||
}
|
||||
await globalState.appController.applyProfile();
|
||||
globalState.appController.savePreferences();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IconsExt{
|
||||
static const IconData target =
|
||||
IconData(0xe900, fontFamily: "Icons");
|
||||
}
|
||||
@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return -1; // 这行理论上不会执行到,但为了完整性保留
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:fl_clash/models/models.dart' hide Process;
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
import 'system.dart';
|
||||
import 'windows.dart';
|
||||
|
||||
class AutoLaunch {
|
||||
static AutoLaunch? _instance;
|
||||
@@ -26,62 +24,18 @@ class AutoLaunch {
|
||||
return await launchAtStartup.isEnabled();
|
||||
}
|
||||
|
||||
Future<bool> get windowsIsEnable async {
|
||||
final res = await Process.run(
|
||||
'schtasks',
|
||||
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
|
||||
runInShell: true,
|
||||
);
|
||||
return res.stdout.toString().contains(Platform.resolvedExecutable);
|
||||
}
|
||||
|
||||
Future<bool> enable() async {
|
||||
if (Platform.isWindows) {
|
||||
await windowsDisable();
|
||||
}
|
||||
return await launchAtStartup.enable();
|
||||
}
|
||||
|
||||
windowsDisable() async {
|
||||
final res = await Process.run(
|
||||
'schtasks',
|
||||
[
|
||||
'/Delete',
|
||||
'/TN',
|
||||
appName,
|
||||
'/F',
|
||||
],
|
||||
runInShell: true,
|
||||
);
|
||||
return res.exitCode == 0;
|
||||
}
|
||||
|
||||
Future<bool> windowsEnable() async {
|
||||
await disable();
|
||||
return await windows?.registerTask(appName) ?? false;
|
||||
}
|
||||
|
||||
Future<bool> disable() async {
|
||||
return await launchAtStartup.disable();
|
||||
}
|
||||
|
||||
updateStatus(AutoLaunchState state) async {
|
||||
final isAdminAutoLaunch = state.isAdminAutoLaunch;
|
||||
final isAutoLaunch = state.isAutoLaunch;
|
||||
if (Platform.isWindows && isAdminAutoLaunch) {
|
||||
if (await windowsIsEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch) {
|
||||
final isEnable = await windowsEnable();
|
||||
if (!isEnable) {
|
||||
enable();
|
||||
}
|
||||
} else {
|
||||
windowsDisable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (await isEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch == true) {
|
||||
updateStatus(bool value) async {
|
||||
final isEnable = await this.isEnable;
|
||||
if (isEnable == value) return;
|
||||
if (value == true) {
|
||||
enable();
|
||||
} else {
|
||||
disable();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
typedef InstallConfigCallBack = void Function(String url);
|
||||
|
||||
@@ -17,7 +17,7 @@ class LinkManager {
|
||||
initAppLinksListen(installConfigCallBack) async {
|
||||
debugPrint("initAppLinksListen");
|
||||
destroy();
|
||||
subscription = _appLinks.uriLinkStream.listen(
|
||||
subscription = _appLinks.allUriLinkStream.listen(
|
||||
(uri) {
|
||||
debugPrint('onAppLink: $uri');
|
||||
if (uri.host == 'install-config') {
|
||||
@@ -31,7 +31,8 @@ class LinkManager {
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
destroy(){
|
||||
if (subscription != null) {
|
||||
subscription?.cancel();
|
||||
subscription = null;
|
||||
|
||||
@@ -2,23 +2,4 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,26 +3,24 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Measure {
|
||||
final TextScaler _textScale;
|
||||
late BuildContext context;
|
||||
Measure.of(this.context);
|
||||
|
||||
Measure.of(this.context)
|
||||
: _textScale = TextScaler.linear(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
);
|
||||
final _textScaleFactor =
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||
|
||||
Size computeTextSize(Text text) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text.data, style: text.style),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScale,
|
||||
textScaler: TextScaler.linear(_textScaleFactor),
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
)..layout();
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
late BuildContext context;
|
||||
|
||||
double? _bodyMediumHeight;
|
||||
Size? _bodyLargeSize;
|
||||
double? _bodySmallHeight;
|
||||
double? _labelSmallHeight;
|
||||
double? _labelMediumHeight;
|
||||
@@ -32,27 +30,17 @@ 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;
|
||||
@@ -62,7 +50,7 @@ class Measure {
|
||||
double get labelSmallHeight {
|
||||
_labelSmallHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
"",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
).height;
|
||||
@@ -72,7 +60,7 @@ class Measure {
|
||||
double get labelMediumHeight {
|
||||
_labelMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
"",
|
||||
style: context.textTheme.labelMedium,
|
||||
),
|
||||
).height;
|
||||
@@ -82,7 +70,7 @@ class Measure {
|
||||
double get titleLargeHeight {
|
||||
_titleLargeHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
"",
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
).height;
|
||||
@@ -92,7 +80,7 @@ class Measure {
|
||||
double get titleMediumHeight {
|
||||
_titleMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
"",
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
).height;
|
||||
|
||||
@@ -44,7 +44,7 @@ class Navigation {
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.storage),
|
||||
icon: Icon(Icons.swap_vert_circle),
|
||||
label: "resources",
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
return await Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
@@ -85,7 +83,7 @@ class Other {
|
||||
if (charA == charB) {
|
||||
return sortByChar(a.substring(1), b.substring(1));
|
||||
} else {
|
||||
return charA.compareToLower(charB);
|
||||
return charA.compareTo(charB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,20 +99,12 @@ class Other {
|
||||
}
|
||||
}
|
||||
|
||||
String getTrayIconPath({
|
||||
required Brightness brightness,
|
||||
}) {
|
||||
if(Platform.isMacOS){
|
||||
return "assets/images/icon_white.png";
|
||||
}
|
||||
final suffix = Platform.isWindows ? "ico" : "png";
|
||||
String getTrayIconPath() {
|
||||
if (Platform.isWindows) {
|
||||
return "assets/images/icon.$suffix";
|
||||
return "assets/images/icon.ico";
|
||||
} else {
|
||||
return "assets/images/icon_monochrome.png";
|
||||
}
|
||||
return switch (brightness) {
|
||||
Brightness.dark => "assets/images/icon_white.$suffix",
|
||||
Brightness.light => "assets/images/icon_black.$suffix",
|
||||
};
|
||||
}
|
||||
|
||||
int compareVersions(String version1, String version2) {
|
||||
@@ -140,12 +130,6 @@ 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;
|
||||
@@ -174,27 +158,25 @@ class Other {
|
||||
if (disposition == null) return null;
|
||||
final parseValue = HeaderValue.parse(disposition);
|
||||
final parameters = parseValue.parameters;
|
||||
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 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 fileNameKey = parameters.keys
|
||||
.firstWhere((key) => key == "filename", orElse: () => "");
|
||||
if (fileNameKey.isEmpty) return null;
|
||||
return parameters[fileNameKey];
|
||||
}
|
||||
|
||||
FlutterView getScreen() {
|
||||
return WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
double getViewWidth() {
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
final size = view.physicalSize / view.devicePixelRatio;
|
||||
return size.width;
|
||||
}
|
||||
|
||||
List<String> parseReleaseBody(String? body) {
|
||||
if (body == null) return [];
|
||||
const pattern = r'- \s*(.*)';
|
||||
const pattern = r'- (.+?)\. \[.+?\]';
|
||||
final regex = RegExp(pattern);
|
||||
return regex
|
||||
.allMatches(body)
|
||||
@@ -209,31 +191,23 @@ class Other {
|
||||
return ViewMode.desktop;
|
||||
}
|
||||
|
||||
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
|
||||
final columns = max((viewWidth / 300).ceil(), 2);
|
||||
return switch (proxiesLayout) {
|
||||
ProxiesLayout.tight => columns + 1,
|
||||
ProxiesLayout.standard => columns,
|
||||
ProxiesLayout.loose => columns - 1,
|
||||
int getColumns(ViewMode viewMode, int currentColumns) {
|
||||
final targetColumnsArray = viewModeColumnsMap[viewMode]!;
|
||||
if (targetColumnsArray.contains(currentColumns)) {
|
||||
return currentColumns;
|
||||
}
|
||||
return targetColumnsArray.first;
|
||||
}
|
||||
|
||||
String getColumnsTextForInt(int number){
|
||||
return switch(number){
|
||||
1 => appLocalizations.oneColumn,
|
||||
2 => appLocalizations.twoColumns,
|
||||
3 => appLocalizations.threeColumns,
|
||||
4 => appLocalizations.fourColumns,
|
||||
int() => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
||||
int getProfilesColumns(double viewWidth) {
|
||||
return max((viewWidth / 400).floor(), 1);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -8,12 +8,10 @@ import 'constant.dart';
|
||||
|
||||
class AppPath {
|
||||
static AppPath? _instance;
|
||||
Completer<Directory> dataDir = Completer();
|
||||
Completer<Directory> downloadDir = Completer();
|
||||
Completer<Directory> tempDir = Completer();
|
||||
late String appDirPath;
|
||||
Completer<Directory> cacheDir = Completer();
|
||||
|
||||
// Future<Directory> _createDesktopCacheDir() async {
|
||||
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
|
||||
// final dir = Directory(path);
|
||||
// if (await dir.exists()) {
|
||||
// await dir.create(recursive: true);
|
||||
@@ -22,15 +20,8 @@ class AppPath {
|
||||
// }
|
||||
|
||||
AppPath._internal() {
|
||||
appDirPath = join(dirname(Platform.resolvedExecutable));
|
||||
getApplicationSupportDirectory().then((value) {
|
||||
dataDir.complete(value);
|
||||
});
|
||||
getTemporaryDirectory().then((value){
|
||||
tempDir.complete(value);
|
||||
});
|
||||
getDownloadsDirectory().then((value) {
|
||||
downloadDir.complete(value);
|
||||
cacheDir.complete(value);
|
||||
});
|
||||
// if (Platform.isAndroid) {
|
||||
// getApplicationSupportDirectory().then((value) {
|
||||
@@ -48,18 +39,13 @@ class AppPath {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<String> getDownloadDirPath() async {
|
||||
final directory = await downloadDir.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getHomeDirPath() async {
|
||||
final directory = await dataDir.future;
|
||||
final directory = await cacheDir.future;
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<String> getProfilesPath() async {
|
||||
final directory = await dataDir.future;
|
||||
final directory = await cacheDir.future;
|
||||
return join(directory.path, profilesDirectoryName);
|
||||
}
|
||||
|
||||
@@ -68,11 +54,6 @@ 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();
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class Picker {
|
||||
Future<PlatformFile?> pickerFile() async {
|
||||
Future<PlatformFile?> pickerConfigFile() async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
allowMultiple: false,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
);
|
||||
return filePickerResult?.files.first;
|
||||
}
|
||||
|
||||
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
fileName: fileName,
|
||||
initialDirectory: await appPath.getDownloadDirPath(),
|
||||
bytes: Platform.isAndroid ? bytes : null,
|
||||
Future<PlatformFile?> pickerGeoDataFile() async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
allowMultiple: false,
|
||||
);
|
||||
if (!Platform.isAndroid && path != null) {
|
||||
final file = await File(path).create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
}
|
||||
return path;
|
||||
return filePickerResult?.files.first;
|
||||
}
|
||||
|
||||
Future<String?> pickerConfigQRCode() async {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
@@ -29,8 +28,7 @@ class Preferences {
|
||||
try {
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return null;
|
||||
throw e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +48,7 @@ class Preferences {
|
||||
try {
|
||||
return Config.fromJson(configMap);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
return null;
|
||||
throw e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,37 @@
|
||||
import 'package:fl_clash/common/system.dart';
|
||||
import 'package:proxy/proxy.dart';
|
||||
import 'package:fl_clash/common/datetime.dart';
|
||||
import 'package:fl_clash/plugins/proxy.dart';
|
||||
import 'package:proxy/proxy.dart' as proxy_plugin;
|
||||
import 'package:proxy/proxy_platform_interface.dart';
|
||||
|
||||
final proxy = system.isDesktop ? Proxy() : null;
|
||||
class ProxyManager {
|
||||
static ProxyManager? _instance;
|
||||
late ProxyPlatform _proxy;
|
||||
|
||||
ProxyManager._internal() {
|
||||
_proxy = proxy ?? proxy_plugin.Proxy();
|
||||
}
|
||||
|
||||
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
||||
|
||||
DateTime? get startTime => _proxy.startTime;
|
||||
|
||||
Future<bool?> startProxy({required int port}) async {
|
||||
return await _proxy.startProxy(port);
|
||||
}
|
||||
|
||||
Future<bool?> stopProxy() async {
|
||||
return await _proxy.stopProxy();
|
||||
}
|
||||
|
||||
Future<DateTime?> updateStartTime() async {
|
||||
if (_proxy is! Proxy) return null;
|
||||
return await (_proxy as Proxy).updateStartTime();
|
||||
}
|
||||
|
||||
factory ProxyManager() {
|
||||
_instance ??= ProxyManager._internal();
|
||||
return _instance!;
|
||||
}
|
||||
}
|
||||
|
||||
final proxyManager = ProxyManager();
|
||||
|
||||
@@ -1,56 +1,66 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/models/ip.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class Request {
|
||||
late final Dio _dio;
|
||||
String? userAgent;
|
||||
int? _port;
|
||||
bool _isStart = false;
|
||||
|
||||
Request() {
|
||||
_dio = Dio();
|
||||
_dio.options = BaseOptions(
|
||||
headers: {"User-Agent": globalState.appController.clashConfig.globalUa},
|
||||
);
|
||||
_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(
|
||||
url,
|
||||
options: Options(
|
||||
headers: {
|
||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
||||
},
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
httpTimeoutDuration * 6,
|
||||
httpTimeoutDuration * 2,
|
||||
);
|
||||
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",
|
||||
@@ -79,10 +89,7 @@ class Request {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
)
|
||||
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
|
||||
.timeout(
|
||||
httpTimeoutDuration,
|
||||
);
|
||||
@@ -90,9 +97,6 @@ class Request {
|
||||
return source.value(response.data!);
|
||||
}
|
||||
} catch (e) {
|
||||
if (cancelToken?.isCancelled == true) {
|
||||
throw "cancelled";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +25,3 @@ 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
110
lib/common/service.dart
Normal file
110
lib/common/service.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
typedef CreateServiceNative = IntPtr Function(
|
||||
IntPtr hSCManager,
|
||||
Pointer<Utf16> lpServiceName,
|
||||
Pointer<Utf16> lpDisplayName,
|
||||
Uint32 dwDesiredAccess,
|
||||
Uint32 dwServiceType,
|
||||
Uint32 dwStartType,
|
||||
Uint32 dwErrorControl,
|
||||
Pointer<Utf16> lpBinaryPathName,
|
||||
Pointer<Utf16> lpLoadOrderGroup,
|
||||
Pointer<Uint32> lpdwTagId,
|
||||
Pointer<Utf16> lpDependencies,
|
||||
Pointer<Utf16> lpServiceStartName,
|
||||
Pointer<Utf16> lpPassword,
|
||||
);
|
||||
|
||||
typedef CreateServiceDart = int Function(
|
||||
int hSCManager,
|
||||
Pointer<Utf16> lpServiceName,
|
||||
Pointer<Utf16> lpDisplayName,
|
||||
int dwDesiredAccess,
|
||||
int dwServiceType,
|
||||
int dwStartType,
|
||||
int dwErrorControl,
|
||||
Pointer<Utf16> lpBinaryPathName,
|
||||
Pointer<Utf16> lpLoadOrderGroup,
|
||||
Pointer<Uint32> lpdwTagId,
|
||||
Pointer<Utf16> lpDependencies,
|
||||
Pointer<Utf16> lpServiceStartName,
|
||||
Pointer<Utf16> lpPassword,
|
||||
);
|
||||
|
||||
const _SERVICE_ALL_ACCESS = 0xF003F;
|
||||
|
||||
const _SERVICE_WIN32_OWN_PROCESS = 0x00000010;
|
||||
|
||||
const _SERVICE_AUTO_START = 0x00000002;
|
||||
|
||||
const _SERVICE_ERROR_NORMAL = 0x00000001;
|
||||
|
||||
typedef GetLastErrorNative = Uint32 Function();
|
||||
typedef GetLastErrorDart = int Function();
|
||||
|
||||
class Service {
|
||||
static Service? _instance;
|
||||
late DynamicLibrary _advapi32;
|
||||
|
||||
Service._internal() {
|
||||
_advapi32 = DynamicLibrary.open('advapi32.dll');
|
||||
}
|
||||
|
||||
factory Service() {
|
||||
_instance ??= Service._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> createService() async {
|
||||
final int scManager = OpenSCManager(nullptr, nullptr, _SERVICE_ALL_ACCESS);
|
||||
if (scManager == 0) return;
|
||||
final serviceName = 'FlClash Service'.toNativeUtf16();
|
||||
final displayName = 'FlClash Service'.toNativeUtf16();
|
||||
final binaryPathName = "C:\\Application\\Clash.Verge_1.6.6_x64_portable\\resources\\clash-verge-service.exe".toNativeUtf16();
|
||||
final createService =
|
||||
_advapi32.lookupFunction<CreateServiceNative, CreateServiceDart>(
|
||||
'CreateServiceW',
|
||||
);
|
||||
final getLastError = DynamicLibrary.open('kernel32.dll')
|
||||
.lookupFunction<GetLastErrorNative, GetLastErrorDart>('GetLastError');
|
||||
|
||||
final serviceHandle = createService(
|
||||
scManager,
|
||||
serviceName,
|
||||
displayName,
|
||||
_SERVICE_ALL_ACCESS,
|
||||
_SERVICE_WIN32_OWN_PROCESS,
|
||||
_SERVICE_AUTO_START,
|
||||
_SERVICE_ERROR_NORMAL,
|
||||
binaryPathName,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
);
|
||||
|
||||
print("serviceHandle $serviceHandle");
|
||||
|
||||
final errorCode = GetLastError();
|
||||
print('Error code: $errorCode');
|
||||
|
||||
final result = StartService(serviceHandle, 0, nullptr);
|
||||
|
||||
if (result == 0) {
|
||||
print('Failed to start the service.');
|
||||
} else {
|
||||
print('Service started successfully.');
|
||||
}
|
||||
|
||||
calloc.free(serviceName);
|
||||
calloc.free(displayName);
|
||||
calloc.free(binaryPathName);
|
||||
}
|
||||
}
|
||||
|
||||
final service = Platform.isWindows ? Service() : null;
|
||||
@@ -1,50 +1,5 @@
|
||||
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);
|
||||
}
|
||||
|
||||
int compareToLower(String other) {
|
||||
return toLowerCase().compareTo(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -19,22 +18,6 @@ class System {
|
||||
bool get isDesktop =>
|
||||
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
|
||||
get isAdmin async {
|
||||
if (!Platform.isWindows) return false;
|
||||
final result = await Process.run('net', ['session'], runInShell: true);
|
||||
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();
|
||||
|
||||
24
lib/common/window.dart
Executable file → Normal file
24
lib/common/window.dart
Executable file → Normal file
@@ -1,13 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:windows_single_instance/windows_single_instance.dart';
|
||||
|
||||
import 'protocol.dart';
|
||||
import 'system.dart';
|
||||
|
||||
class Window {
|
||||
init(WindowProps props, int version) async {
|
||||
init(WindowProps props) async {
|
||||
if (Platform.isWindows) {
|
||||
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
|
||||
protocol.register("clash");
|
||||
@@ -18,10 +20,18 @@ class Window {
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
minimumSize: const Size(380, 500),
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
);
|
||||
if (!Platform.isMacOS || version > 10) {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
if (props.left != null || props.top != null) {
|
||||
await windowManager.setPosition(
|
||||
Offset(props.left ?? 0, props.top ?? 0),
|
||||
);
|
||||
} else {
|
||||
await windowManager.setAlignment(Alignment.center);
|
||||
}
|
||||
// if(Platform.isWindows){
|
||||
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
// }
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.setPreventClose(true);
|
||||
});
|
||||
@@ -30,11 +40,6 @@ class Window {
|
||||
show() async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
return await windowManager.isVisible();
|
||||
}
|
||||
|
||||
close() async {
|
||||
@@ -43,7 +48,6 @@ class Window {
|
||||
|
||||
hide() async {
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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;
|
||||
late DynamicLibrary _shell32;
|
||||
|
||||
Windows._internal() {
|
||||
_shell32 = DynamicLibrary.open('shell32.dll');
|
||||
}
|
||||
|
||||
factory Windows() {
|
||||
_instance ??= Windows._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool runas(String command, String arguments) {
|
||||
final commandPtr = command.toNativeUtf16();
|
||||
final argumentsPtr = arguments.toNativeUtf16();
|
||||
final operationPtr = 'runas'.toNativeUtf16();
|
||||
|
||||
final shellExecute = _shell32.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
Int32 nShowCmd),
|
||||
int Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
int nShowCmd)>('ShellExecuteW');
|
||||
|
||||
final result = shellExecute(
|
||||
nullptr,
|
||||
operationPtr,
|
||||
commandPtr,
|
||||
argumentsPtr,
|
||||
nullptr,
|
||||
1,
|
||||
);
|
||||
|
||||
calloc.free(commandPtr);
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
if (result <= 32) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
@@ -1,74 +1,63 @@
|
||||
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:path/path.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'clash/core.dart';
|
||||
import 'common/common.dart';
|
||||
import 'models/models.dart';
|
||||
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;
|
||||
late Function applyProfileDebounce;
|
||||
late Function savePreferencesDebounce;
|
||||
|
||||
AppController(this.context) {
|
||||
appState = context.read<AppState>();
|
||||
config = context.read<Config>();
|
||||
clashConfig = context.read<ClashConfig>();
|
||||
appFlowingState = context.read<AppFlowingState>();
|
||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||
await updateClashConfig();
|
||||
});
|
||||
savePreferencesDebounce = debounce<Function()>(() async {
|
||||
await savePreferences();
|
||||
});
|
||||
applyProfileDebounce = debounce<Function()>(() async {
|
||||
await applyProfile(isPrue: true);
|
||||
});
|
||||
addCheckIpNumDebounce = debounce(() {
|
||||
appState.checkIpNum++;
|
||||
});
|
||||
updateGroupDebounce = debounce(() async {
|
||||
await updateGroups();
|
||||
});
|
||||
measure = Measure.of(context);
|
||||
}
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
Future<void> updateSystemProxy(bool isStart) async {
|
||||
if (isStart) {
|
||||
await globalState.handleStart();
|
||||
await globalState.startSystemProxy(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
updateRunTime();
|
||||
updateTraffic();
|
||||
globalState.updateFunctionLists = [
|
||||
updateRunTime,
|
||||
updateTraffic,
|
||||
];
|
||||
if (!Platform.isAndroid) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
if (Platform.isAndroid) return;
|
||||
await applyProfile(isPrue: true);
|
||||
} else {
|
||||
await globalState.handleStop();
|
||||
await globalState.stopSystemProxy();
|
||||
clashCore.resetTraffic();
|
||||
appFlowingState.traffics = [];
|
||||
appFlowingState.totalTraffic = Traffic();
|
||||
appFlowingState.runTime = null;
|
||||
appState.traffics = [];
|
||||
appState.totalTraffic = Traffic();
|
||||
appState.runTime = null;
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
@@ -78,19 +67,18 @@ class AppController {
|
||||
}
|
||||
|
||||
updateRunTime() {
|
||||
final startTime = globalState.startTime;
|
||||
if (startTime != null) {
|
||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||
if (proxyManager.startTime != null) {
|
||||
final startTimeStamp = proxyManager.startTime!.millisecondsSinceEpoch;
|
||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
||||
appState.runTime = nowTimeStamp - startTimeStamp;
|
||||
} else {
|
||||
appFlowingState.runTime = null;
|
||||
appState.runTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateTraffic() {
|
||||
globalState.updateTraffic(
|
||||
appFlowingState: appFlowingState,
|
||||
appState: appState,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,39 +90,30 @@ class AppController {
|
||||
|
||||
deleteProfile(String id) async {
|
||||
config.deleteProfileById(id);
|
||||
clashCore.clearEffect(id);
|
||||
final profilePath = await appPath.getProfilePath(id);
|
||||
if (profilePath == null) return;
|
||||
clashCore.clearEffect(profilePath);
|
||||
if (config.currentProfileId == id) {
|
||||
if (config.profiles.isNotEmpty) {
|
||||
final updateId = config.profiles.first.id;
|
||||
changeProfile(updateId);
|
||||
} else {
|
||||
changeProfile(null);
|
||||
updateStatus(false);
|
||||
updateSystemProxy(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProviders() {
|
||||
globalState.updateProviders(appState);
|
||||
}
|
||||
|
||||
Future<void> updateProfile(Profile profile) async {
|
||||
final newProfile = await profile.update();
|
||||
config.setProfile(
|
||||
newProfile.copyWith(isUpdating: false),
|
||||
);
|
||||
await profile.update();
|
||||
config.setProfile(await profile.update());
|
||||
}
|
||||
|
||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.updateClashConfig(
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
);
|
||||
});
|
||||
await globalState.updateClashConfig(
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
);
|
||||
}
|
||||
|
||||
Future applyProfile({bool isPrue = false}) async {
|
||||
@@ -161,6 +140,9 @@ class AppController {
|
||||
changeProfile(String? value) async {
|
||||
if (value == config.currentProfileId) return;
|
||||
config.currentProfileId = value;
|
||||
await applyProfile();
|
||||
appState.delayMap = {};
|
||||
saveConfigPreferences();
|
||||
}
|
||||
|
||||
autoUpdateProfiles() async {
|
||||
@@ -177,7 +159,7 @@ class AppController {
|
||||
try {
|
||||
updateProfile(profile);
|
||||
} catch (e) {
|
||||
appFlowingState.addLog(
|
||||
appState.addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: e.toString(),
|
||||
@@ -205,8 +187,17 @@ class AppController {
|
||||
}
|
||||
|
||||
savePreferences() async {
|
||||
debugPrint("[APP] savePreferences");
|
||||
await saveConfigPreferences();
|
||||
await saveClashConfigPreferences();
|
||||
}
|
||||
|
||||
saveConfigPreferences() async {
|
||||
debugPrint("saveConfigPreferences");
|
||||
await preferences.saveConfig(config);
|
||||
}
|
||||
|
||||
saveClashConfigPreferences() async {
|
||||
debugPrint("saveClashConfigPreferences");
|
||||
await preferences.saveClashConfig(clashConfig);
|
||||
}
|
||||
|
||||
@@ -223,9 +214,9 @@ class AppController {
|
||||
}
|
||||
|
||||
handleBackOrExit() async {
|
||||
if (config.appSetting.minimizeOnExit) {
|
||||
if (config.isMinimizeOnExit) {
|
||||
if (system.isDesktop) {
|
||||
await savePreferencesDebounce();
|
||||
await savePreferences();
|
||||
}
|
||||
await system.back();
|
||||
} else {
|
||||
@@ -234,24 +225,23 @@ class AppController {
|
||||
}
|
||||
|
||||
handleExit() async {
|
||||
await updateStatus(false);
|
||||
await proxy?.stopProxy();
|
||||
await updateSystemProxy(false);
|
||||
await savePreferences();
|
||||
clashCore.shutdown();
|
||||
system.exit();
|
||||
}
|
||||
|
||||
updateLogStatus() {
|
||||
if (config.appSetting.openLogs) {
|
||||
if (config.openLogs) {
|
||||
clashCore.startLog();
|
||||
} else {
|
||||
clashCore.stopLog();
|
||||
appFlowingState.logs = [];
|
||||
appState.logs = [];
|
||||
}
|
||||
}
|
||||
|
||||
autoCheckUpdate() async {
|
||||
if (!config.appSetting.autoCheckUpdate) return;
|
||||
if (!config.autoCheckUpdate) return;
|
||||
final res = await request.checkForUpdate();
|
||||
checkUpdateResultHandle(data: res);
|
||||
}
|
||||
@@ -300,21 +290,35 @@ class AppController {
|
||||
}
|
||||
|
||||
init() async {
|
||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
handleExit();
|
||||
}
|
||||
updateLogStatus();
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
if (!config.silentLaunch) {
|
||||
window?.show();
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
globalState.updateStartTime();
|
||||
}
|
||||
if (globalState.isStart) {
|
||||
await updateStatus(true);
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted == true) {
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
}, title: appLocalizations.init);
|
||||
} else {
|
||||
await updateStatus(config.appSetting.autoRun);
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
}
|
||||
await afterInit();
|
||||
}
|
||||
|
||||
afterInit() async {
|
||||
await proxyManager.updateStartTime();
|
||||
if (proxyManager.isStart) {
|
||||
await updateSystemProxy(true);
|
||||
} else {
|
||||
await updateSystemProxy(config.autoRun);
|
||||
}
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
@@ -329,7 +333,7 @@ class AppController {
|
||||
return;
|
||||
}
|
||||
appState.currentLabel = appState.currentNavigationItems[index].label;
|
||||
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
|
||||
if ((config.isAnimateToPage || hasAnimate)) {
|
||||
globalState.pageController?.animateToPage(
|
||||
index,
|
||||
duration: kTabScrollDuration,
|
||||
@@ -378,53 +382,6 @@ class AppController {
|
||||
);
|
||||
}
|
||||
|
||||
showSnackBar(String message) {
|
||||
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);
|
||||
@@ -446,7 +403,7 @@ class AppController {
|
||||
}
|
||||
|
||||
addProfileFormFile() async {
|
||||
final platformFile = await globalState.safeRun(picker.pickerFile);
|
||||
final platformFile = await globalState.safeRun(picker.pickerConfigFile);
|
||||
final bytes = platformFile?.bytes;
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
@@ -474,6 +431,8 @@ class AppController {
|
||||
addProfileFormURL(url);
|
||||
}
|
||||
|
||||
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
|
||||
|
||||
updateViewWidth(double width) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appState.viewWidth = width;
|
||||
@@ -483,10 +442,7 @@ class AppController {
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)
|
||||
..sort(
|
||||
(a, b) => other.sortByChar(
|
||||
other.getPinyin(a.name),
|
||||
other.getPinyin(b.name),
|
||||
),
|
||||
(a, b) => other.sortByChar(a.name, b.name),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -511,7 +467,7 @@ class AppController {
|
||||
}
|
||||
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
||||
return switch (config.proxiesStyle.sortType) {
|
||||
return switch (config.proxiesSortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(proxies),
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
@@ -524,134 +480,4 @@ class AppController {
|
||||
config.currentSelectedMap[groupName] ?? '') ??
|
||||
'';
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
);
|
||||
}
|
||||
|
||||
updateSystemProxy() {
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
systemProxy: !config.networkProps.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();
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
updateTray([bool focus = false]) async {
|
||||
globalState.updateTray(
|
||||
appState: appState,
|
||||
appFlowingState: appFlowingState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
focus: focus,
|
||||
);
|
||||
}
|
||||
|
||||
recoveryData(
|
||||
List<int> data,
|
||||
RecoveryOption recoveryOption,
|
||||
) async {
|
||||
final archive = await Isolate.run<Archive>(() {
|
||||
final zipDecoder = ZipDecoder();
|
||||
return zipDecoder.decodeBytes(data);
|
||||
});
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final configs =
|
||||
archive.files.where((item) => item.name.endsWith(".json")).toList();
|
||||
final profiles =
|
||||
archive.files.where((item) => !item.name.endsWith(".json"));
|
||||
final configIndex =
|
||||
configs.indexWhere((config) => config.name == "config.json");
|
||||
final clashConfigIndex =
|
||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
|
||||
final configFile = configs[configIndex];
|
||||
final clashConfigFile = configs[clashConfigIndex];
|
||||
final tempConfig = Config.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(configFile.content),
|
||||
),
|
||||
);
|
||||
final tempClashConfig = ClashConfig.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(clashConfigFile.content),
|
||||
),
|
||||
);
|
||||
for (final profile in profiles) {
|
||||
final filePath = join(homeDirPath, profile.name);
|
||||
final file = File(filePath);
|
||||
await file.create(recursive: true);
|
||||
await file.writeAsBytes(profile.content);
|
||||
}
|
||||
if (recoveryOption == RecoveryOption.onlyProfiles) {
|
||||
config.update(tempConfig, RecoveryOption.onlyProfiles);
|
||||
} else {
|
||||
config.update(tempConfig, RecoveryOption.all);
|
||||
clashConfig.update(tempClashConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// 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 }
|
||||
@@ -15,10 +11,6 @@ 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;
|
||||
@@ -60,8 +52,6 @@ enum TunStack { gvisor, system, mixed }
|
||||
|
||||
enum AccessControlMode { acceptSelected, rejectSelected }
|
||||
|
||||
enum AccessSortType { none, name, time }
|
||||
|
||||
enum ProfileType { file, url }
|
||||
|
||||
enum ResultType { success, error }
|
||||
@@ -94,87 +84,4 @@ enum CommonCardType { plain, filled }
|
||||
|
||||
enum ProxiesType { tab, list }
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
enum RouteMode {
|
||||
bypassPrivate,
|
||||
config,
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class AboutFragment extends StatelessWidget {
|
||||
title: const Text("Telegram"),
|
||||
onTap: () {
|
||||
globalState.openUrl(
|
||||
"https://t.me/FlClash",
|
||||
"https://t.me/+G-veVtwBOl4wODc1",
|
||||
);
|
||||
},
|
||||
trailing: const Icon(Icons.launch),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
@@ -7,9 +6,15 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
extension AccessControlExtension on AccessControl {
|
||||
List<String> get currentList => switch (mode) {
|
||||
AccessControlMode.acceptSelected => acceptList,
|
||||
AccessControlMode.rejectSelected => rejectList,
|
||||
};
|
||||
}
|
||||
|
||||
class AccessFragment extends StatefulWidget {
|
||||
const AccessFragment({super.key});
|
||||
|
||||
@@ -18,100 +23,319 @@ class AccessFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccessFragmentState extends State<AccessFragment> {
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
final packagesListenable = ValueNotifier<List<Package>>([]);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateInitList();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
if (appState.packages.isEmpty) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
appState.packages = await app?.getPackages() ?? [];
|
||||
});
|
||||
}
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
packagesListenable.value = await app?.getPackages() ?? [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_updateInitList() {
|
||||
final accessControl = globalState.appController.config.accessControl;
|
||||
acceptList = accessControl.acceptList;
|
||||
rejectList = accessControl.rejectList;
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
packagesListenable.dispose();
|
||||
}
|
||||
|
||||
Widget _buildSearchButton() {
|
||||
Widget _buildAppProxyModePopup() {
|
||||
final items = [
|
||||
CommonPopupMenuItem(
|
||||
action: AccessControlMode.rejectSelected,
|
||||
label: appLocalizations.blacklistMode,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
action: AccessControlMode.acceptSelected,
|
||||
label: appLocalizations.whitelistMode,
|
||||
),
|
||||
];
|
||||
return Selector<Config, AccessControlMode>(
|
||||
selector: (_, config) => config.accessControl.mode,
|
||||
builder: (context, mode, __) {
|
||||
return CommonPopupMenu<AccessControlMode>.radio(
|
||||
icon: Icon(
|
||||
Icons.mode_standby,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
items: items,
|
||||
onSelected: (value) {
|
||||
final config = context.read<Config>();
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
mode: value,
|
||||
);
|
||||
},
|
||||
selectedValue: mode,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterSystemAppButton() {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
||||
builder: (context, isFilterSystemApp, __) {
|
||||
final tooltip = isFilterSystemApp
|
||||
? appLocalizations.cancelFilterSystemApp
|
||||
: appLocalizations.filterSystemApp;
|
||||
return IconButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: () {
|
||||
final config = context.read<Config>();
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
isFilterSystemApp: !isFilterSystemApp,
|
||||
);
|
||||
},
|
||||
icon: isFilterSystemApp
|
||||
? const Icon(Icons.filter_list_off)
|
||||
: const Icon(Icons.filter_list),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchButton(List<Package> packages) {
|
||||
return IconButton(
|
||||
tooltip: appLocalizations.search,
|
||||
onPressed: () {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: AccessControlSearchDelegate(
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
packages: packages,
|
||||
),
|
||||
).then((_) => setState(() {
|
||||
_updateInitList();
|
||||
}));
|
||||
).then((_) => {setState(() {})});
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedAllButton({
|
||||
required bool isSelectedAll,
|
||||
required List<String> allValueList,
|
||||
}) {
|
||||
final tooltip = isSelectedAll
|
||||
? appLocalizations.cancelSelectAll
|
||||
: appLocalizations.selectAll;
|
||||
return IconButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
final isAccept =
|
||||
config.accessControl.mode == AccessControlMode.acceptSelected;
|
||||
if (isSelectedAll) {
|
||||
config.accessControl = switch (isAccept) {
|
||||
true => config.accessControl.copyWith(
|
||||
acceptList: [],
|
||||
),
|
||||
false => config.accessControl.copyWith(
|
||||
rejectList: [],
|
||||
),
|
||||
};
|
||||
} else {
|
||||
config.accessControl = switch (isAccept) {
|
||||
true => config.accessControl.copyWith(
|
||||
acceptList: allValueList,
|
||||
),
|
||||
false => config.accessControl.copyWith(
|
||||
rejectList: allValueList,
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
icon: isSelectedAll
|
||||
? const Icon(Icons.deselect)
|
||||
: const Icon(Icons.select_all),
|
||||
);
|
||||
}
|
||||
// Widget _buildSelectedAllButton({
|
||||
// required bool isAccessControl,
|
||||
// required bool isSelectedAll,
|
||||
// required List<String> allValueList,
|
||||
// }) {
|
||||
// final tooltip = isSelectedAll
|
||||
// ? appLocalizations.cancelSelectAll
|
||||
// : appLocalizations.selectAll;
|
||||
// return AbsorbPointer(
|
||||
// absorbing: !isAccessControl,
|
||||
// child: FloatingActionButton(
|
||||
// tooltip: tooltip,
|
||||
// onPressed: () {
|
||||
// final config = globalState.appController.config;
|
||||
// final isAccept =
|
||||
// config.accessControl.mode == AccessControlMode.acceptSelected;
|
||||
//
|
||||
// if (isSelectedAll) {
|
||||
// config.accessControl = switch (isAccept) {
|
||||
// true => config.accessControl.copyWith(
|
||||
// acceptList: [],
|
||||
// ),
|
||||
// false => config.accessControl.copyWith(
|
||||
// rejectList: [],
|
||||
// ),
|
||||
// };
|
||||
// } else {
|
||||
// config.accessControl = switch (isAccept) {
|
||||
// true => config.accessControl.copyWith(
|
||||
// acceptList: allValueList,
|
||||
// ),
|
||||
// false => config.accessControl.copyWith(
|
||||
// rejectList: allValueList,
|
||||
// ),
|
||||
// };
|
||||
// }
|
||||
// },
|
||||
// child: isSelectedAll
|
||||
// ? const Icon(Icons.deselect)
|
||||
// : const Icon(Icons.select_all),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget _buildSettingButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AccessControlWidget(
|
||||
context: context,
|
||||
Widget _buildPackageList() {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: packagesListenable,
|
||||
builder: (_, packages, ___) {
|
||||
final accessControl = globalState.appController.config.accessControl;
|
||||
final acceptList = accessControl.acceptList;
|
||||
final rejectList = accessControl.rejectList;
|
||||
final acceptPackages = packages.sorted((a, b) {
|
||||
final isSelectA = acceptList.contains(a.packageName);
|
||||
final isSelectB = acceptList.contains(b.packageName);
|
||||
if (isSelectA && isSelectB) return 0;
|
||||
if (isSelectA) return -1;
|
||||
if (isSelectB) return 1;
|
||||
return 0;
|
||||
});
|
||||
final rejectPackages = packages.sorted((a, b) {
|
||||
final isSelectA = rejectList.contains(a.packageName);
|
||||
final isSelectB = rejectList.contains(b.packageName);
|
||||
if (isSelectA && isSelectB) return 0;
|
||||
if (isSelectA) return -1;
|
||||
if (isSelectB) return 1;
|
||||
return 0;
|
||||
});
|
||||
return Selector<Config, PackageListSelectorState>(
|
||||
selector: (_, config) => PackageListSelectorState(
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
final accessControl = state.accessControl;
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final isFilterSystemApp = accessControl.isFilterSystemApp;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages =
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptPackages
|
||||
: rejectPackages;
|
||||
final currentList = accessControl.currentList;
|
||||
final currentPackages = isFilterSystemApp
|
||||
? packages
|
||||
.where((element) => element.isSystem == false)
|
||||
.toList()
|
||||
: packages;
|
||||
final packageNameList =
|
||||
currentPackages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
final describe =
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? appLocalizations.accessControlAllowDesc
|
||||
: appLocalizations.accessControlNotAllowDesc;
|
||||
return DisabledMask(
|
||||
status: !isAccessControl,
|
||||
child: Column(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: !isAccessControl,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: 16,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
appLocalizations.selected,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"${valueList.length}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(describe),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(currentPackages)),
|
||||
Flexible(child: _buildFilterSystemAppButton()),
|
||||
Flexible(child: _buildAppProxyModePopup()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: FadeBox(
|
||||
key: const Key("fade_box"),
|
||||
child: currentPackages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: currentPackages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final package = currentPackages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value:
|
||||
valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config =
|
||||
globalState.appController.config;
|
||||
if (accessControlMode ==
|
||||
AccessControlMode.acceptSelected) {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.tune),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,170 +372,7 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Selector<AppState, List<Package>>(
|
||||
selector: (_, appState) => appState.packages,
|
||||
builder: (_, packages, ___) {
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
packages: appState.packages,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
final accessControl = state.accessControl;
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptList
|
||||
: rejectList,
|
||||
);
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList =
|
||||
packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
final describe =
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? appLocalizations.accessControlAllowDesc
|
||||
: appLocalizations.accessControlNotAllowDesc;
|
||||
return DisabledMask(
|
||||
status: !isAccessControl,
|
||||
child: Column(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: !isAccessControl,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: 16,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
appLocalizations.selected,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"${valueList.length}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(describe),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSelectedAllButton(
|
||||
isSelectedAll: valueList.length ==
|
||||
packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSettingButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: packages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: packages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value:
|
||||
valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config =
|
||||
globalState.appController.config;
|
||||
if (accessControlMode ==
|
||||
AccessControlMode.acceptSelected) {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
child: _buildPackageList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -378,14 +439,23 @@ class PackageListItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
class AccessControlSearchDelegate extends SearchDelegate {
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
final List<Package> packages;
|
||||
|
||||
AccessControlSearchDelegate({
|
||||
required this.acceptList,
|
||||
required this.rejectList,
|
||||
required this.packages,
|
||||
});
|
||||
|
||||
List<Package> get _results {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return packages
|
||||
.where(
|
||||
(package) =>
|
||||
package.label.toLowerCase().contains(lowQuery) ||
|
||||
package.packageName.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) {
|
||||
return [
|
||||
@@ -415,39 +485,26 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _packageList() {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
packages: appState.packages,
|
||||
Widget _packageList(List<Package> packages) {
|
||||
return Selector<Config, PackageListSelectorState>(
|
||||
selector: (_, config) => PackageListSelectorState(
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
final accessControl = state.accessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
accessControlMode == AccessControlMode.acceptSelected
|
||||
? acceptList
|
||||
: rejectList,
|
||||
);
|
||||
final queryPackages = packages
|
||||
.where(
|
||||
(package) =>
|
||||
package.label.toLowerCase().contains(lowQuery) ||
|
||||
package.packageName.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||
final packageNameList =
|
||||
this.packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
return DisabledMask(
|
||||
status: !isAccessControl,
|
||||
child: ListView.builder(
|
||||
itemCount: queryPackages.length,
|
||||
itemCount: packages.length,
|
||||
itemBuilder: (_, index) {
|
||||
final package = queryPackages[index];
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
@@ -485,268 +542,6 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return _packageList();
|
||||
}
|
||||
}
|
||||
|
||||
class AccessControlWidget extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
|
||||
const AccessControlWidget({
|
||||
super.key,
|
||||
required this.context,
|
||||
});
|
||||
|
||||
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
||||
return switch (mode) {
|
||||
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
||||
AccessControlMode.rejectSelected => Icons.block_outlined,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTextWithAccessControlMode(AccessControlMode mode) {
|
||||
return switch (mode) {
|
||||
AccessControlMode.acceptSelected => appLocalizations.whitelistMode,
|
||||
AccessControlMode.rejectSelected => appLocalizations.blacklistMode,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTextWithAccessSortType(AccessSortType type) {
|
||||
return switch (type) {
|
||||
AccessSortType.none => appLocalizations.defaultText,
|
||||
AccessSortType.name => appLocalizations.name,
|
||||
AccessSortType.time => appLocalizations.time,
|
||||
};
|
||||
}
|
||||
|
||||
IconData _getIconWithProxiesSortType(AccessSortType type) {
|
||||
return switch (type) {
|
||||
AccessSortType.none => Icons.sort,
|
||||
AccessSortType.name => Icons.sort_by_alpha,
|
||||
AccessSortType.time => Icons.timeline,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) {
|
||||
return switch (isFilterSystemApp) {
|
||||
true => appLocalizations.onlyOtherApps,
|
||||
false => appLocalizations.allApps,
|
||||
};
|
||||
}
|
||||
|
||||
List<Widget> _buildModeSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.mode,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessControlMode>(
|
||||
selector: (_, config) => config.accessControl.mode,
|
||||
builder: (_, accessControlMode, __) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in AccessControlMode.values)
|
||||
SettingInfoCard(
|
||||
Info(
|
||||
label: _getTextWithAccessControlMode(item),
|
||||
iconData: _getIconWithAccessControlMode(item),
|
||||
),
|
||||
isSelected: accessControlMode == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
mode: item,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSortSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.sort,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessSortType>(
|
||||
selector: (_, config) => config.accessControl.sort,
|
||||
builder: (_, accessSortType, __) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in AccessSortType.values)
|
||||
SettingInfoCard(
|
||||
Info(
|
||||
label: _getTextWithAccessSortType(item),
|
||||
iconData: _getIconWithProxiesSortType(item),
|
||||
),
|
||||
isSelected: accessSortType == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
sort: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSourceSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.source,
|
||||
items: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
||||
builder: (_, isFilterSystemApp, __) {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in [false, true])
|
||||
SettingTextCard(
|
||||
_getTextWithIsFilterSystemApp(item),
|
||||
isSelected: isFilterSystemApp == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
isFilterSystemApp: item,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_intelligentSelected() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final config = globalState.appController.config;
|
||||
final accessControl = config.accessControl;
|
||||
final packageNames = appState.packages
|
||||
.where(
|
||||
(item) =>
|
||||
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
||||
)
|
||||
.map((item) => item.packageName);
|
||||
Navigator.of(context).pop();
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
final selectedPackageNames =
|
||||
(await commonScaffoldState?.loadingRun<List<String>>(
|
||||
() async {
|
||||
return await app?.getChinaPackageNames() ?? [];
|
||||
},
|
||||
))
|
||||
?.toSet() ??
|
||||
{};
|
||||
final acceptList = packageNames
|
||||
.where((item) => !selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
final rejectList = packageNames
|
||||
.where((item) => selectedPackageNames.contains(item))
|
||||
.toList();
|
||||
config.accessControl = accessControl.copyWith(
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
);
|
||||
}
|
||||
|
||||
_copyToClipboard() async {
|
||||
await globalState.safeRun(() {
|
||||
final data = globalState.appController.config.accessControl.toJson();
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: json.encode(data),
|
||||
),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
_pasteToClipboard() async {
|
||||
await globalState.safeRun(() async {
|
||||
final config = globalState.appController.config;
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text == null) return;
|
||||
config.accessControl = AccessControl.fromJson(
|
||||
json.decode(text),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
List<Widget> _buildActionSetting() {
|
||||
return generateSection(
|
||||
title: appLocalizations.action,
|
||||
items: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
spacing: 16,
|
||||
children: [
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.auto_awesome),
|
||||
label: appLocalizations.intelligentSelected,
|
||||
onPressed: _intelligentSelected,
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.paste),
|
||||
label: appLocalizations.clipboardImport,
|
||||
onPressed: _pasteToClipboard,
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.content_copy),
|
||||
label: appLocalizations.clipboardExport,
|
||||
onPressed: _copyToClipboard,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildModeSetting(),
|
||||
..._buildSortSetting(),
|
||||
..._buildSourceSetting(),
|
||||
..._buildActionSetting(),
|
||||
],
|
||||
),
|
||||
);
|
||||
return _packageList(_results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,84 +8,6 @@ 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});
|
||||
|
||||
@@ -98,18 +20,17 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> items = [
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.minimizeOnExit,
|
||||
selector: (_, config) => config.isMinimizeOnExit,
|
||||
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.appSetting = config.appSetting.copyWith(
|
||||
minimizeOnExit: value,
|
||||
);
|
||||
config.isMinimizeOnExit = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -117,57 +38,52 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
),
|
||||
if (system.isDesktop)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoLaunch,
|
||||
selector: (_, config) => config.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 = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoLaunch: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.autoLaunch = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if(Platform.isWindows)
|
||||
const AdminAutoLaunchItem(),
|
||||
if (system.isDesktop)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.silentLaunch,
|
||||
selector: (_, config) => config.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 = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
silentLaunch: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.silentLaunch = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoRun,
|
||||
selector: (_, config) => config.autoRun,
|
||||
builder: (_, autoRun, child) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.start),
|
||||
title: Text(appLocalizations.autoRun),
|
||||
subtitle: Text(appLocalizations.autoRunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: autoRun,
|
||||
onChanged: (bool value) {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoRun: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.autoRun = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -175,18 +91,17 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.hidden,
|
||||
selector: (_, config) => config.isExclude,
|
||||
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 = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
hidden: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.isExclude = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -194,56 +109,52 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.isAnimateToPage,
|
||||
selector: (_, config) => config.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 = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
isAnimateToPage: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.isAnimateToPage = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.openLogs,
|
||||
selector: (_, config) => config.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 = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
openLogs: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.openLogs = value;
|
||||
globalState.appController.updateLogStatus();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const CloseConnectionsSwitch(),
|
||||
const UsageSwitch(),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoCheckUpdate,
|
||||
selector: (_, config) => config.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 = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoCheckUpdate: value,
|
||||
);
|
||||
final config = context.read<Config>();
|
||||
config.autoCheckUpdate = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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/models.dart';
|
||||
import 'package:fl_clash/models/config.dart';
|
||||
import 'package:fl_clash/models/dav.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/fade_box.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
@@ -11,9 +10,16 @@ import 'package:fl_clash/widgets/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class BackupAndRecovery extends StatelessWidget {
|
||||
class BackupAndRecovery extends StatefulWidget {
|
||||
const BackupAndRecovery({super.key});
|
||||
|
||||
@override
|
||||
State<BackupAndRecovery> createState() => _BackupAndRecoveryState();
|
||||
}
|
||||
|
||||
class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
DAVClient? _client;
|
||||
|
||||
_showAddWebDAV(DAV? dav) async {
|
||||
await globalState.showCommonDialog<String>(
|
||||
child: WebDAVFormDialog(
|
||||
@@ -22,15 +28,11 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
_backupOnWebDAV(BuildContext context, DAVClient client) async {
|
||||
_backup() async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final backupData = await globalState.appController.backupData();
|
||||
return await client.backup(Uint8List.fromList(backupData));
|
||||
},
|
||||
title: appLocalizations.backup,
|
||||
);
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(() async {
|
||||
return await _client?.backup();
|
||||
});
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.backup,
|
||||
@@ -38,20 +40,11 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
_recoveryOnWebDAV(
|
||||
BuildContext context,
|
||||
DAVClient client,
|
||||
RecoveryOption recoveryOption,
|
||||
) async {
|
||||
_recovery(RecoveryOption recoveryOption) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final data = await client.recovery();
|
||||
await globalState.appController.recoveryData(data, recoveryOption);
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.recovery,
|
||||
);
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(() async {
|
||||
return await _client?.recovery(recoveryOption: recoveryOption);
|
||||
});
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.recovery,
|
||||
@@ -59,66 +52,12 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
_handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async {
|
||||
_handleRecovery() async {
|
||||
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
|
||||
child: const RecoveryOptionsDialog(),
|
||||
);
|
||||
if (recoveryOption == null || !context.mounted) return;
|
||||
_recoveryOnWebDAV(context, client, recoveryOption);
|
||||
}
|
||||
|
||||
_backupOnLocal(BuildContext context) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
final backupData = await globalState.appController.backupData();
|
||||
final value = await picker.saveFile(
|
||||
other.getBackupFileName(),
|
||||
Uint8List.fromList(backupData),
|
||||
);
|
||||
if (value == null) return false;
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.backup,
|
||||
);
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.backup,
|
||||
message: TextSpan(text: appLocalizations.backupSuccess),
|
||||
);
|
||||
}
|
||||
|
||||
_recoveryOnLocal(
|
||||
BuildContext context,
|
||||
RecoveryOption recoveryOption,
|
||||
) async {
|
||||
final file = await picker.pickerFile();
|
||||
final data = file?.bytes;
|
||||
if (data == null || !context.mounted) return;
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||
() async {
|
||||
await globalState.appController.recoveryData(
|
||||
List<int>.from(data),
|
||||
recoveryOption,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
title: appLocalizations.recovery,
|
||||
);
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.recovery,
|
||||
message: TextSpan(text: appLocalizations.recoverySuccess),
|
||||
);
|
||||
}
|
||||
|
||||
_handleRecoveryOnLocal(BuildContext context) async {
|
||||
final recoveryOption = await globalState.showCommonDialog<RecoveryOption>(
|
||||
child: const RecoveryOptionsDialog(),
|
||||
);
|
||||
if (recoveryOption == null || !context.mounted) return;
|
||||
_recoveryOnLocal(context, recoveryOption);
|
||||
if (recoveryOption == null) return;
|
||||
_recovery(recoveryOption);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -126,11 +65,12 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
return Selector<Config, DAV?>(
|
||||
selector: (_, config) => config.dav,
|
||||
builder: (_, dav, __) {
|
||||
final client = dav != null ? DAVClient(dav) : null;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.remote),
|
||||
if (dav == null)
|
||||
if (dav == null) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(
|
||||
title: appLocalizations.account,
|
||||
),
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: Text(appLocalizations.noInfo),
|
||||
@@ -143,113 +83,95 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
appLocalizations.bind,
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: TooltipText(
|
||||
text: Text(
|
||||
dav.user,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(appLocalizations.connectivity),
|
||||
FutureBuilder<bool>(
|
||||
future: client!.pingCompleter.future,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeBox(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: snapshot.data == true
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.edit,
|
||||
),
|
||||
),
|
||||
),
|
||||
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);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.remoteBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
||||
),
|
||||
],
|
||||
ListHeader(title: appLocalizations.local),
|
||||
);
|
||||
}
|
||||
_client = DAVClient(dav);
|
||||
final pingFuture = _client!.pingCompleter.future;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.account),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.localBackupDesc),
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: TooltipText(
|
||||
text: Text(
|
||||
dav.user,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(appLocalizations.connectivity),
|
||||
FutureBuilder<bool>(
|
||||
future: pingFuture,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeBox(
|
||||
key: const Key("fade_box_1"),
|
||||
child: snapshot.connectionState == ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: snapshot.data == true
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.edit,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnLocal(context);
|
||||
FutureBuilder<bool>(
|
||||
future: pingFuture,
|
||||
builder: (_, snapshot) {
|
||||
return FadeBox(
|
||||
key: const Key("fade_box_2"),
|
||||
child: snapshot.data == true
|
||||
? Column(
|
||||
children: [
|
||||
ListHeader(
|
||||
title: appLocalizations.backupAndRecovery),
|
||||
ListItem(
|
||||
onTap: _backup,
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.backupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: _handleRecovery,
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.recoveryDesc),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(),
|
||||
);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -258,50 +180,6 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class RecoveryOptionsDialog extends StatefulWidget {
|
||||
const RecoveryOptionsDialog({super.key});
|
||||
|
||||
@override
|
||||
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
|
||||
}
|
||||
|
||||
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
|
||||
_handleOnTab(RecoveryOption? value) {
|
||||
if (value == null) return;
|
||||
Navigator.of(context).pop(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.recovery),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 16,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.onlyProfiles);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryProfiles),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.all);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryAll),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WebDAVFormDialog extends StatefulWidget {
|
||||
final DAV? dav;
|
||||
|
||||
@@ -360,7 +238,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
maxLines: 5,
|
||||
maxLines: 2,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.link),
|
||||
@@ -435,3 +313,47 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecoveryOptionsDialog extends StatefulWidget {
|
||||
const RecoveryOptionsDialog({super.key});
|
||||
|
||||
@override
|
||||
State<RecoveryOptionsDialog> createState() => _RecoveryOptionsDialogState();
|
||||
}
|
||||
|
||||
class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
|
||||
_handleOnTab(RecoveryOption? value) {
|
||||
if (value == null) return;
|
||||
Navigator.of(context).pop(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(appLocalizations.recovery),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 16,
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.onlyProfiles);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryProfiles),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleOnTab(RecoveryOption.all);
|
||||
},
|
||||
title: Text(appLocalizations.recoveryAll),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
591
lib/fragments/config.dart
Normal file
591
lib/fragments/config.dart
Normal file
@@ -0,0 +1,591 @@
|
||||
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;
|
||||
globalState.appController.updateClashConfigDebounce();
|
||||
} 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;
|
||||
appController.updateClashConfigDebounce();
|
||||
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;
|
||||
appController.updateClashConfigDebounce();
|
||||
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;
|
||||
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.allowBypass,
|
||||
builder: (_, allowBypass, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.arrow_forward_outlined),
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.allowBypass = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.settings_ethernet),
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.config.systemProxy = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.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<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;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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;
|
||||
globalState.appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
|
||||
builder: (_, tcpConcurrent, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.tcpConcurrent = value;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.geodataLoader == geodataLoaderMemconservative,
|
||||
builder: (_, memconservative, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: memconservative,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geodataLoader = value
|
||||
? geodataLoaderMemconservative
|
||||
: geodataLoaderStandard;
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.externalController.isNotEmpty,
|
||||
builder: (_, hasExternalController, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.externalController =
|
||||
value ? defaultExternalController : '';
|
||||
appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
globalState.appController.updateClashConfigDebounce();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@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),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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,
|
||||
extendPageWidth: 360,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,739 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
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();
|
||||
@@ -1,415 +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:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VPNItem extends StatelessWidget {
|
||||
const VPNItem({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<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.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 AllowBypassItem extends StatelessWidget {
|
||||
const AllowBypassItem({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 VpnSystemProxyItem extends StatelessWidget {
|
||||
const VpnSystemProxyItem({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 SystemProxyItem extends StatelessWidget {
|
||||
const SystemProxyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.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 networkProps = config.networkProps;
|
||||
config.networkProps = networkProps.copyWith(
|
||||
systemProxy: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Item extends StatelessWidget {
|
||||
const Ipv6Item({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});
|
||||
|
||||
_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 config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
Icons.replay,
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.bypassDomain),
|
||||
subtitle: Text(appLocalizations.bypassDomainDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
title: appLocalizations.bypassDomain,
|
||||
widget: Selector<Config, List<String>>(
|
||||
selector: (_, config) => config.networkProps.bypassDomain,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (context, bypassDomain, __) {
|
||||
_initActions(context);
|
||||
return ListPage(
|
||||
title: appLocalizations.bypassDomain,
|
||||
items: bypassDomain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteModeItem extends StatelessWidget {
|
||||
const RouteModeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, RouteMode>(
|
||||
selector: (_, clashConfig) => clashConfig.routeMode,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<RouteMode>.options(
|
||||
title: Text(appLocalizations.routeMode),
|
||||
subtitle: Text(Intl.message("routeMode_${value.name}")),
|
||||
delegate: OptionsDelegate<RouteMode>(
|
||||
title: appLocalizations.routeMode,
|
||||
options: RouteMode.values,
|
||||
onChanged: (RouteMode? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.routeMode = value;
|
||||
},
|
||||
textBuilder: (routeMode) => Intl.message(
|
||||
"routeMode_${routeMode.name}",
|
||||
),
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteAddressItem extends StatelessWidget {
|
||||
const RouteAddressItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
|
||||
builder: (_, value, child) {
|
||||
if (value) {
|
||||
return child!;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
child: ListItem.open(
|
||||
title: Text(appLocalizations.routeAddress),
|
||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
title: appLocalizations.routeAddress,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!stringListEquality.equals(prev, next),
|
||||
builder: (context, routeAddress, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.routeAddress,
|
||||
items: routeAddress,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.includeRouteAddress =
|
||||
Set<String>.from(items).toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final networkItems = [
|
||||
if (Platform.isAndroid) const VPNItem(),
|
||||
if (Platform.isAndroid)
|
||||
...generateSection(
|
||||
title: "VPN",
|
||||
items: [
|
||||
const SystemProxyItem(),
|
||||
const AllowBypassItem(),
|
||||
const Ipv6Item(),
|
||||
],
|
||||
),
|
||||
if (system.isDesktop)
|
||||
...generateSection(
|
||||
title: appLocalizations.system,
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
...generateSection(
|
||||
title: appLocalizations.options,
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
const RouteModeItem(),
|
||||
const RouteAddressItem(),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
|
||||
import 'package:fl_clash/fragments/dashboard/status_button.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'network_detection.dart';
|
||||
import 'network_speed.dart';
|
||||
import 'outbound_mode.dart';
|
||||
import 'start_button.dart';
|
||||
import 'network_speed.dart';
|
||||
import 'traffic_usage.dart';
|
||||
|
||||
class DashboardFragment extends StatefulWidget {
|
||||
@@ -23,74 +19,43 @@ class DashboardFragment extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
_initFab(bool isCurrent) {
|
||||
if (!isCurrent) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState =
|
||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActiveBuilder(
|
||||
label: "dashboard",
|
||||
builder: (isCurrent, child) {
|
||||
_initFab(isCurrent);
|
||||
return child!;
|
||||
},
|
||||
return FloatLayout(
|
||||
floatingWidget: const FloatWrapper(
|
||||
child: StartButton(),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Selector<AppState, double>(
|
||||
selector: (_, appState) => appState.viewWidth,
|
||||
builder: (_, viewWidth, ___) {
|
||||
final columns = max(4 * ((viewWidth / 350).ceil()), 8);
|
||||
final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
|
||||
// final viewMode = other.getViewMode(viewWidth);
|
||||
// final isDesktop = viewMode == ViewMode.desktop;
|
||||
return Grid(
|
||||
crossAxisCount: columns,
|
||||
crossAxisCount: max(4 * ((viewWidth / 350).ceil()), 8),
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: [
|
||||
const GridItem(
|
||||
children: const [
|
||||
GridItem(
|
||||
crossAxisCellCount: 8,
|
||||
child: NetworkSpeed(),
|
||||
),
|
||||
// if (Platform.isAndroid)
|
||||
// GridItem(
|
||||
// crossAxisCellCount: switchCount,
|
||||
// child: const VPNSwitch(),
|
||||
// ),
|
||||
if (system.isDesktop) ...[
|
||||
if (Platform.isWindows)
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const TUNButton(),
|
||||
),
|
||||
GridItem(
|
||||
crossAxisCellCount: switchCount,
|
||||
child: const SystemProxyButton(),
|
||||
),
|
||||
],
|
||||
const GridItem(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: OutboundMode(),
|
||||
),
|
||||
const GridItem(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: NetworkDetection(),
|
||||
),
|
||||
const GridItem(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: TrafficUsage(),
|
||||
),
|
||||
const GridItem(
|
||||
GridItem(
|
||||
crossAxisCellCount: 4,
|
||||
child: IntranetIP(),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
@@ -15,67 +13,29 @@ class IntranetIP extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _IntranetIPState extends State<IntranetIP> {
|
||||
final ipNotifier = ValueNotifier<String?>("");
|
||||
late StreamSubscription subscription;
|
||||
|
||||
Future<String> getNetworkType() async {
|
||||
try {
|
||||
final interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
type: InternetAddressType.any,
|
||||
);
|
||||
for (var interface in interfaces) {
|
||||
if (interface.name.toLowerCase().contains('wlan') ||
|
||||
interface.name.toLowerCase().contains('wi-fi')) {
|
||||
return 'WiFi';
|
||||
}
|
||||
if (interface.name.toLowerCase().contains('rmnet') ||
|
||||
interface.name.toLowerCase().contains('ccmni') ||
|
||||
interface.name.toLowerCase().contains('cellular')) {
|
||||
return 'Mobile Data';
|
||||
}
|
||||
}
|
||||
return 'Unknown';
|
||||
} catch (e) {
|
||||
return 'Error';
|
||||
}
|
||||
}
|
||||
final ipNotifier = ValueNotifier<String>("");
|
||||
|
||||
Future<String?> getLocalIpAddress() async {
|
||||
await Future.delayed(animateDuration);
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||
includeLoopback: false,
|
||||
)
|
||||
..sort((a, b) {
|
||||
if (a.isWifi && !b.isWifi) return -1;
|
||||
if (!a.isWifi && b.isWifi) return 1;
|
||||
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
List<NetworkInterface> interfaces = await NetworkInterface.list();
|
||||
for (final interface in interfaces) {
|
||||
final addresses = interface.addresses;
|
||||
if (addresses.isEmpty) {
|
||||
continue;
|
||||
for (final address in interface.addresses) {
|
||||
if (!address.isLoopback) {
|
||||
return address.address;
|
||||
}
|
||||
}
|
||||
addresses.sort((a, b) {
|
||||
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||
return 0;
|
||||
});
|
||||
return addresses.first.address;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
ipNotifier.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
subscription = Connectivity().onConnectivityChanged.listen((_) async {
|
||||
ipNotifier.value = null;
|
||||
debugPrint("[App] Connection change");
|
||||
ipNotifier.value = await getLocalIpAddress() ?? "";
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ipNotifier.value = await getLocalIpAddress() ?? "";
|
||||
});
|
||||
@@ -88,15 +48,17 @@ 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.measure.titleMediumHeight + 24 - 2,
|
||||
height: globalState.appController.measure.titleLargeHeight + 24 - 2,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: ipNotifier,
|
||||
builder: (_, value, __) {
|
||||
return FadeBox(
|
||||
child: value != null
|
||||
child: value.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -105,11 +67,8 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
value.isNotEmpty
|
||||
? value
|
||||
: appLocalizations.noNetwork,
|
||||
style: context
|
||||
.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
value,
|
||||
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -130,11 +89,4 @@ class _IntranetIPState extends State<IntranetIP> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
subscription.cancel();
|
||||
ipNotifier.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:country_flags/country_flags.dart';
|
||||
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';
|
||||
@@ -17,63 +15,28 @@ class NetworkDetection extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
const NetworkDetectionState(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
),
|
||||
);
|
||||
final ipInfoNotifier = ValueNotifier<IpInfo?>(null);
|
||||
final timeoutNotifier = ValueNotifier<bool>(false);
|
||||
bool? _preIsStart;
|
||||
Function? _checkIpDebounce;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
|
||||
_checkIp() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
final isInit = appState.isInit;
|
||||
final isStart = appState.isStart;
|
||||
if (!isInit) return;
|
||||
final isStart = appFlowingState.isStart;
|
||||
timeoutNotifier.value = false;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
_clearSetTimeoutTimer();
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: true,
|
||||
ipInfo: null,
|
||||
);
|
||||
_preIsStart = isStart;
|
||||
if (cancelToken != null) {
|
||||
cancelToken!.cancel();
|
||||
cancelToken = null;
|
||||
}
|
||||
cancelToken = CancelToken();
|
||||
try {
|
||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||
if (ipInfo != null) {
|
||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||
isTesting: false,
|
||||
ipInfo: ipInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_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;
|
||||
ipInfoNotifier.value = null;
|
||||
final ipInfo = await request.checkIp();
|
||||
if (ipInfo == null) {
|
||||
timeoutNotifier.value = true;
|
||||
return;
|
||||
} else {
|
||||
timeoutNotifier.value = false;
|
||||
}
|
||||
ipInfoNotifier.value = ipInfo;
|
||||
}
|
||||
|
||||
_checkIpContainer(Widget child) {
|
||||
@@ -94,28 +57,17 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
networkDetectionState.dispose();
|
||||
}
|
||||
|
||||
String countryCodeToEmoji(String countryCode) {
|
||||
final String code = countryCode.toUpperCase();
|
||||
if (code.length != 2) {
|
||||
return countryCode;
|
||||
}
|
||||
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
|
||||
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
|
||||
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
|
||||
ipInfoNotifier.dispose();
|
||||
timeoutNotifier.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_checkIpDebounce ??= debounce(_checkIp);
|
||||
_checkIpDebounce = debounce(_checkIp);
|
||||
return _checkIpContainer(
|
||||
ValueListenableBuilder<NetworkDetectionState>(
|
||||
valueListenable: networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
ValueListenableBuilder<IpInfo?>(
|
||||
valueListenable: ipInfoNotifier,
|
||||
builder: (_, ipInfo, __) {
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
@@ -136,38 +88,37 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: FadeBox(
|
||||
child: isTesting
|
||||
? Text(
|
||||
appLocalizations.checking,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium,
|
||||
child: ipInfo != null
|
||||
? CountryFlag.fromCountryCode(
|
||||
ipInfo.countryCode,
|
||||
width: 24,
|
||||
height: 24,
|
||||
)
|
||||
: ipInfo != null
|
||||
? Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: globalState
|
||||
.measure.titleMediumHeight,
|
||||
child: Text(
|
||||
countryCodeToEmoji(
|
||||
ipInfo.countryCode),
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: timeoutNotifier,
|
||||
builder: (_, timeout, __) {
|
||||
if (timeout) {
|
||||
return Text(
|
||||
appLocalizations.checkError,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
fontFamily: FontFamily.twEmoji.value,
|
||||
),
|
||||
.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
return TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.checking,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
appLocalizations.checkError,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -175,7 +126,9 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||
height: globalState.appController.measure.titleLargeHeight +
|
||||
24 -
|
||||
2,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||
child: FadeBox(
|
||||
@@ -198,24 +151,28 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
),
|
||||
],
|
||||
)
|
||||
: FadeBox(
|
||||
child: isTesting == false && ipInfo == null
|
||||
? Text(
|
||||
"timeout",
|
||||
style: context.textTheme.titleLarge
|
||||
?.copyWith(color: Colors.red)
|
||||
.toSoftBold
|
||||
.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: timeoutNotifier,
|
||||
builder: (_, timeout, __) {
|
||||
if (timeout) {
|
||||
return Text(
|
||||
"timeout",
|
||||
style: context.textTheme.titleLarge
|
||||
?.copyWith(color: Colors.red)
|
||||
.toSoftBold
|
||||
.toMinus,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
style: bodyMedium,
|
||||
maxLines: 1,
|
||||
);
|
||||
final size = globalState.measure.computeTextSize(valueText);
|
||||
final size = globalState.appController.measure.computeTextSize(valueText);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -114,10 +114,10 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
iconData: Icons.speed,
|
||||
),
|
||||
child: Selector<AppFlowingState, List<Traffic>>(
|
||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||
child: Selector<AppState, List<Traffic>>(
|
||||
selector: (_, appState) => appState.traffics,
|
||||
builder: (_, traffics, __) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
||||
@@ -15,6 +15,7 @@ class OutboundMode extends StatelessWidget {
|
||||
final clashConfig = appController.clashConfig;
|
||||
if (value == null || clashConfig.mode == value) return;
|
||||
clashConfig.mode = value;
|
||||
await appController.updateClashConfig();
|
||||
appController.addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
@@ -27,7 +28,7 @@ class OutboundMode extends StatelessWidget {
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.outboundMode,
|
||||
iconData: Icons.call_split_sharp,
|
||||
iconData: Icons.call_split,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
|
||||
@@ -19,10 +19,9 @@ class _StartButtonState extends State<StartButton>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isStart = globalState.appController.appFlowingState.isStart;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
value: isStart ? 1 : 0,
|
||||
value: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
@@ -35,10 +34,10 @@ class _StartButtonState extends State<StartButton>
|
||||
|
||||
handleSwitchStart() {
|
||||
final appController = globalState.appController;
|
||||
if (isStart == appController.appFlowingState.isStart) {
|
||||
if (isStart == appController.appState.isStart) {
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
appController.updateStatus(isStart);
|
||||
appController.updateSystemProxy(isStart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +50,10 @@ class _StartButtonState extends State<StartButton>
|
||||
}
|
||||
|
||||
Widget _updateControllerContainer(Widget child) {
|
||||
return Selector<AppFlowingState, bool>(
|
||||
selector: (_, appFlowingState) => appFlowingState.isStart,
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.isStart,
|
||||
builder: (_, isStart, child) {
|
||||
if (isStart != this.isStart) {
|
||||
if(isStart != this.isStart){
|
||||
this.isStart = isStart;
|
||||
updateController();
|
||||
}
|
||||
@@ -75,7 +74,7 @@ class _StartButtonState extends State<StartButton>
|
||||
if (!state.isInit || !state.hasProfile) {
|
||||
return Container();
|
||||
}
|
||||
final textWidth = globalState.measure
|
||||
final textWidth = globalState.appController.measure
|
||||
.computeTextSize(
|
||||
Text(
|
||||
other.getTimeDifference(
|
||||
@@ -86,58 +85,58 @@ class _StartButtonState extends State<StartButton>
|
||||
)
|
||||
.width +
|
||||
16;
|
||||
return _updateControllerContainer(
|
||||
AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56 + textWidth * _controller.value,
|
||||
height: 56,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
handleSwitchStart();
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _controller,
|
||||
),
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56 + textWidth * _controller.value,
|
||||
height: 56,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
handleSwitchStart();
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _controller,
|
||||
),
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: OverflowBox(
|
||||
maxWidth: textWidth,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: OverflowBox(
|
||||
maxWidth: textWidth,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Selector<AppFlowingState, int?>(
|
||||
selector: (_, appFlowingState) => appFlowingState.runTime,
|
||||
builder: (_, int? value, __) {
|
||||
final text = other.getTimeText(value);
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
);
|
||||
},
|
||||
child: _updateControllerContainer(
|
||||
Selector<AppState, int?>(
|
||||
selector: (_, appState) => appState.runTime,
|
||||
builder: (_, int? value, __) {
|
||||
final text = other.getTimeText(value);
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'package:fl_clash/common/app_localizations.dart';
|
||||
import 'package:fl_clash/common/system.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../config/network.dart';
|
||||
|
||||
class TUNButton extends StatelessWidget {
|
||||
const TUNButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ButtonContainer(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return generateListView(generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
));
|
||||
},
|
||||
title: appLocalizations.tun,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.tun,
|
||||
iconData: Icons.stacked_line_chart,
|
||||
),
|
||||
child: Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, enable, __) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) => Switch(
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemProxyButton extends StatelessWidget {
|
||||
const SystemProxyButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ButtonContainer(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
title: appLocalizations.systemProxy,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.systemProxy,
|
||||
iconData: Icons.shuffle,
|
||||
),
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) => Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps =
|
||||
config.networkProps.copyWith(systemProxy: value);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonContainer extends StatelessWidget {
|
||||
final Info info;
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const ButtonContainer({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
onPressed: onPressed,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoHeader(
|
||||
info: info,
|
||||
actions: [
|
||||
child,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user