Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfa6d31673 | ||
|
|
89bbbc6864 | ||
|
|
a3e1b38201 | ||
|
|
4e3dc45f13 | ||
|
|
13d31cf708 | ||
|
|
62a7772b92 | ||
|
|
043648f998 | ||
|
|
3eb26e8061 | ||
|
|
5d6bd6466f | ||
|
|
4e766d9407 |
57
.github/release_template.md
vendored
Normal file
57
.github/release_template.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<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,7 +35,6 @@ jobs:
|
|||||||
install: mingw-w64-x86_64-gcc
|
install: mingw-w64-x86_64-gcc
|
||||||
update: true
|
update: true
|
||||||
|
|
||||||
|
|
||||||
- name: Set Mingw64 Env
|
- name: Set Mingw64 Env
|
||||||
if: startsWith(matrix.platform,'windows')
|
if: startsWith(matrix.platform,'windows')
|
||||||
run: |
|
run: |
|
||||||
@@ -102,12 +101,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
|
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
|
||||||
path: ./dist
|
path: ./dist
|
||||||
retention-days: 1
|
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
|
|
||||||
upload-release:
|
upload:
|
||||||
if: ${{ !contains(github.ref, '+') }}
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -124,32 +121,62 @@ jobs:
|
|||||||
pattern: artifact-*
|
pattern: artifact-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Pre Release
|
- name: Generate release
|
||||||
run: |
|
run: |
|
||||||
pip install gitchangelog pystache mustache markdown
|
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||||
pre=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
|
preTag=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
|
||||||
if [ -z "pre" ]; then
|
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||||
echo "init" > release.md
|
sed "s|VERSION|$version|g" ./.github/release_template.md > release.md
|
||||||
else
|
currentTag=""
|
||||||
current="${{ github.ref_name }}"
|
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||||
echo -e "\n\n<details markdown=1><summary>All changes from $current to the latest commit:</summary>\n\n" >> release.md
|
if (( i < ${#tags[@]} )); then
|
||||||
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
|
tag=${tags[$i]}
|
||||||
echo -e "\n\n</details>" >> release.md
|
else
|
||||||
fi
|
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" >> release.md
|
||||||
|
echo "" >> release.md
|
||||||
|
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: Upload
|
||||||
|
if: ${{ contains(github.ref, '+') }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifact
|
||||||
|
path: ./dist
|
||||||
|
retention-days: 7
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
if: ${{ !contains(github.ref, '+') }}
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: ./dist/*
|
files: ./dist/*
|
||||||
body_path: './release.md'
|
body_path: './release.md'
|
||||||
|
|
||||||
- name: Create Fdroid Source Dir
|
- name: Create Fdroid Source Dir
|
||||||
|
if: ${{ !contains(github.ref, '+') }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./tmp
|
mkdir -p ./tmp
|
||||||
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
||||||
echo "Files copied successfully"
|
echo "Files copied successfully"
|
||||||
|
|
||||||
- name: Push to fdroid repo
|
- name: Push to fdroid repo
|
||||||
|
if: ${{ !contains(github.ref, '+') }}
|
||||||
uses: cpina/github-action-push-to-another-repository@v1.7.2
|
uses: cpina/github-action-push-to-another-repository@v1.7.2
|
||||||
env:
|
env:
|
||||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||||
@@ -161,4 +188,4 @@ jobs:
|
|||||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||||
target-branch: action-pr
|
target-branch: action-pr
|
||||||
commit-message: Update from ${{ github.ref_name }}
|
commit-message: Update from ${{ github.ref_name }}
|
||||||
target-directory: /tmp/
|
target-directory: /tmp/
|
||||||
45
.github/workflows/change.yaml
vendored
Normal file
45
.github/workflows/change.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: change
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changelog:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Generate
|
||||||
|
run: |
|
||||||
|
tags=$(git tag --sort=creatordate)
|
||||||
|
previous=""
|
||||||
|
if [ ! -f CHANGELOG.md ]; then
|
||||||
|
echo "" > CHANGELOG.md
|
||||||
|
else
|
||||||
|
previous=$(grep -oP '^## \K.*' CHANGELOG.md | tail -n 1)
|
||||||
|
fi
|
||||||
|
for tag in $tags; do
|
||||||
|
if [ -n "$previous" ]; then
|
||||||
|
echo "## $tag" >> CHANGELOG.md
|
||||||
|
git log --pretty=format:"* %s (%h)" "$previous..$tag" >> CHANGELOG.md
|
||||||
|
echo -e "\n" >> CHANGELOG.md
|
||||||
|
fi
|
||||||
|
previous=$tag
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Commit
|
||||||
|
run: |
|
||||||
|
if !git diff --cached --quiet; then
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "Update Changelog"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,7 +1,7 @@
|
|||||||
[submodule "core/Clash.Meta"]
|
[submodule "core/Clash.Meta"]
|
||||||
path = core/Clash.Meta
|
path = core/Clash.Meta
|
||||||
url = git@github.com:chen08209/Clash.Meta.git
|
url = git@github.com:chen08209/Clash.Meta.git
|
||||||
branch = FlClash
|
branch = FlClash-Alpha
|
||||||
[submodule "plugins/flutter_distributor"]
|
[submodule "plugins/flutter_distributor"]
|
||||||
path = plugins/flutter_distributor
|
path = plugins/flutter_distributor
|
||||||
url = git@github.com:chen08209/flutter_distributor.git
|
url = git@github.com:chen08209/flutter_distributor.git
|
||||||
|
|||||||
630
CHANGELOG.md
Normal file
630
CHANGELOG.md
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
## v0.8.63
|
||||||
|
|
||||||
|
- Fix windows admin auto launch issues
|
||||||
|
|
||||||
|
- Add android vpn options
|
||||||
|
|
||||||
|
- Support proxies icon configuration
|
||||||
|
|
||||||
|
- Optimize android immersion display
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
## v0.8.62
|
||||||
|
|
||||||
|
- Optimize ip detection
|
||||||
|
|
||||||
|
- Support android vpn ipv6 inbound switch
|
||||||
|
|
||||||
|
- Support log export
|
||||||
|
|
||||||
|
- Optimize more details
|
||||||
|
|
||||||
|
- Fix android system dns issues
|
||||||
|
|
||||||
|
- Optimize dns default option
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Update readme
|
||||||
|
|
||||||
|
- Update README.md 2
|
||||||
|
|
||||||
|
- Update README.md 2
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
## v0.8.60
|
||||||
|
|
||||||
|
- Fix build error2
|
||||||
|
|
||||||
|
- Fix build error
|
||||||
|
|
||||||
|
- Support desktop hotkey
|
||||||
|
|
||||||
|
- Support android ipv6 inbound
|
||||||
|
|
||||||
|
- Support android system dns
|
||||||
|
|
||||||
|
- fix some bugs
|
||||||
|
|
||||||
|
## v0.8.59
|
||||||
|
|
||||||
|
- Fix delete profile error
|
||||||
|
|
||||||
|
## v0.8.58
|
||||||
|
|
||||||
|
- Fix submit error 2
|
||||||
|
|
||||||
|
- Fix submit error
|
||||||
|
|
||||||
|
- Optimize DNS strategy
|
||||||
|
|
||||||
|
- Fix the problem that the tray is not displayed in some cases
|
||||||
|
|
||||||
|
- Optimize tray
|
||||||
|
|
||||||
|
- Update core
|
||||||
|
|
||||||
|
- Fix some error
|
||||||
|
|
||||||
|
## v0.8.57
|
||||||
|
|
||||||
|
- Fix tun update issues
|
||||||
|
|
||||||
|
- Add DNS override
|
||||||
|
- Fixed some bugs
|
||||||
|
- Optimize more detail
|
||||||
|
|
||||||
|
- Add Hosts override
|
||||||
|
|
||||||
|
## v0.8.56
|
||||||
|
|
||||||
|
- fix android tip error
|
||||||
|
- fix windows auto launch error
|
||||||
|
|
||||||
|
## v0.8.55
|
||||||
|
|
||||||
|
- Fix windows tray issues
|
||||||
|
|
||||||
|
- Optimize windows logic
|
||||||
|
|
||||||
|
- Optimize app logic
|
||||||
|
|
||||||
|
- Support windows administrator auto launch
|
||||||
|
|
||||||
|
- Support android close vpn
|
||||||
|
|
||||||
|
## v0.8.53
|
||||||
|
|
||||||
|
- Change flutter version
|
||||||
|
|
||||||
|
- Support profiles sort
|
||||||
|
|
||||||
|
- Support windows country flags display
|
||||||
|
|
||||||
|
- Optimize proxies page and profiles page columns
|
||||||
|
|
||||||
|
## v0.8.52
|
||||||
|
|
||||||
|
- Update flutter version
|
||||||
|
|
||||||
|
- Update version
|
||||||
|
|
||||||
|
- Update timeout time
|
||||||
|
|
||||||
|
- Update access control page
|
||||||
|
|
||||||
|
- Fix bug
|
||||||
|
|
||||||
|
## v0.8.51
|
||||||
|
|
||||||
|
- Optimize provider page
|
||||||
|
|
||||||
|
- Optimize delay test
|
||||||
|
|
||||||
|
- Support local backup and recovery
|
||||||
|
|
||||||
|
- Fix android tile service issues
|
||||||
|
|
||||||
|
## v0.8.49
|
||||||
|
|
||||||
|
- Fix linux core build error
|
||||||
|
|
||||||
|
- Add proxy-only traffic statistics
|
||||||
|
|
||||||
|
- Update core
|
||||||
|
|
||||||
|
- Optimize more details
|
||||||
|
|
||||||
|
- Merge pull request #140 from txyyh/main
|
||||||
|
|
||||||
|
- 添加自建 F-Droid 仓库相关 workflow
|
||||||
|
- Rename readme fingerprint
|
||||||
|
|
||||||
|
- Rename workflow deploy repo name
|
||||||
|
|
||||||
|
- Add download guide to README
|
||||||
|
|
||||||
|
- Add push release files to fdroid-repo
|
||||||
|
|
||||||
|
## v0.8.48
|
||||||
|
|
||||||
|
- Optimize proxies page
|
||||||
|
|
||||||
|
- Fix ua issues
|
||||||
|
|
||||||
|
- Optimize more details
|
||||||
|
|
||||||
|
## v0.8.47
|
||||||
|
|
||||||
|
- Fix windows build error
|
||||||
|
|
||||||
|
## v0.8.46
|
||||||
|
|
||||||
|
- Update app icon
|
||||||
|
|
||||||
|
- Fix desktop backup error
|
||||||
|
|
||||||
|
- Optimize request ua
|
||||||
|
|
||||||
|
- Change android icon
|
||||||
|
|
||||||
|
- Optimize dashboard
|
||||||
|
|
||||||
|
## v0.8.44
|
||||||
|
|
||||||
|
- Remove request validate certificate
|
||||||
|
|
||||||
|
- Sync core
|
||||||
|
|
||||||
|
## v0.8.43
|
||||||
|
|
||||||
|
- Fix windows error
|
||||||
|
|
||||||
|
## v0.8.42
|
||||||
|
|
||||||
|
- Fix setup.dart error
|
||||||
|
|
||||||
|
- Fix android system proxy not effective
|
||||||
|
|
||||||
|
- Add macos arm64
|
||||||
|
|
||||||
|
## v0.8.41
|
||||||
|
|
||||||
|
- Optimize proxies page
|
||||||
|
|
||||||
|
- Support mouse drag scroll
|
||||||
|
|
||||||
|
- Adjust desktop ui
|
||||||
|
|
||||||
|
- Revert "Fix android vpn issues"
|
||||||
|
|
||||||
|
- This reverts commit 891977408e6938e2acd74e9b9adb959c48c79988.
|
||||||
|
|
||||||
|
## v0.8.40
|
||||||
|
|
||||||
|
- Fix android vpn issues
|
||||||
|
|
||||||
|
- Fix android vpn issues
|
||||||
|
|
||||||
|
- Rollback partial modification
|
||||||
|
|
||||||
|
## v0.8.39
|
||||||
|
|
||||||
|
- Fix the problem that ui can't be synchronized when android vpn is occupied by an external
|
||||||
|
|
||||||
|
- Override default socksPort,port
|
||||||
|
|
||||||
|
## v0.8.38
|
||||||
|
|
||||||
|
- Fix fab issues
|
||||||
|
|
||||||
|
## v0.8.37
|
||||||
|
|
||||||
|
- Update version
|
||||||
|
|
||||||
|
- Fix the problem that vpn cannot be started in some cases
|
||||||
|
|
||||||
|
- Fix the problem that geodata url does not take effect
|
||||||
|
|
||||||
|
## v0.8.36
|
||||||
|
|
||||||
|
- Update ua
|
||||||
|
|
||||||
|
- Fix change outbound mode without check ip issues
|
||||||
|
|
||||||
|
- Separate android ui and vpn
|
||||||
|
|
||||||
|
- Fix url validate issues 2
|
||||||
|
|
||||||
|
- Add android hidden from the recent task
|
||||||
|
|
||||||
|
- Add geoip file
|
||||||
|
|
||||||
|
- Support modify geoData URL
|
||||||
|
|
||||||
|
## v0.8.35
|
||||||
|
|
||||||
|
- Fix url validate issues
|
||||||
|
|
||||||
|
- Fix check ip performance problem
|
||||||
|
|
||||||
|
- Optimize resources page
|
||||||
|
|
||||||
|
## v0.8.34
|
||||||
|
|
||||||
|
- Add ua selector
|
||||||
|
|
||||||
|
- Support modify test url
|
||||||
|
|
||||||
|
- Optimize android proxy
|
||||||
|
|
||||||
|
- Fix the error that async proxy provider could not selected the proxy
|
||||||
|
|
||||||
|
## v0.8.33
|
||||||
|
|
||||||
|
- Fix android proxy error
|
||||||
|
|
||||||
|
- Fix submit error
|
||||||
|
|
||||||
|
- Add windows tun
|
||||||
|
|
||||||
|
- Optimize android proxy
|
||||||
|
|
||||||
|
- Optimize change profile
|
||||||
|
|
||||||
|
- Update application ua
|
||||||
|
|
||||||
|
- Optimize delay test
|
||||||
|
|
||||||
|
## v0.8.32
|
||||||
|
|
||||||
|
- Fix android repeated request notification issues
|
||||||
|
|
||||||
|
## v0.8.31
|
||||||
|
|
||||||
|
- Fix memory overflow issues
|
||||||
|
|
||||||
|
## v0.8.30
|
||||||
|
|
||||||
|
- Optimize proxies expansion panel 2
|
||||||
|
|
||||||
|
- Fix android scan qrcode error
|
||||||
|
|
||||||
|
## v0.8.29
|
||||||
|
|
||||||
|
- Optimize proxies expansion panel
|
||||||
|
|
||||||
|
- Fix text error
|
||||||
|
|
||||||
|
## v0.8.28
|
||||||
|
|
||||||
|
- Optimize proxy
|
||||||
|
|
||||||
|
- Optimize delayed sorting performance
|
||||||
|
|
||||||
|
- Add expansion panel proxies page
|
||||||
|
|
||||||
|
- Support to adjust the proxy card size
|
||||||
|
|
||||||
|
- Support to adjust proxies columns number
|
||||||
|
|
||||||
|
- Fix autoRun show issues
|
||||||
|
|
||||||
|
- Fix Android 10 issues
|
||||||
|
|
||||||
|
- Optimize ip show
|
||||||
|
|
||||||
|
## v0.8.26
|
||||||
|
|
||||||
|
- Add intranet IP display
|
||||||
|
|
||||||
|
- Add connections page
|
||||||
|
|
||||||
|
- Add search in connections, requests
|
||||||
|
|
||||||
|
- Add keyword search in connections, requests, logs
|
||||||
|
|
||||||
|
- Add basic viewing editing capabilities
|
||||||
|
|
||||||
|
- Optimize update profile
|
||||||
|
|
||||||
|
## v0.8.25
|
||||||
|
|
||||||
|
- Update version
|
||||||
|
|
||||||
|
- Fix the problem of excessive memory usage in traffic usage.
|
||||||
|
|
||||||
|
- Add lightBlue theme color
|
||||||
|
|
||||||
|
- Fix start unable to update profile issues
|
||||||
|
|
||||||
|
- Fix flashback caused by process
|
||||||
|
|
||||||
|
## v0.8.23
|
||||||
|
|
||||||
|
- Add build version
|
||||||
|
|
||||||
|
- Optimize quick start
|
||||||
|
|
||||||
|
- Update system default option
|
||||||
|
|
||||||
|
## v0.8.22
|
||||||
|
|
||||||
|
- Update build.yml
|
||||||
|
|
||||||
|
- Fix android vpn close issues
|
||||||
|
|
||||||
|
- Add requests page
|
||||||
|
|
||||||
|
- Fix checkUpdate dark mode style error
|
||||||
|
|
||||||
|
- Fix quickStart error open app
|
||||||
|
|
||||||
|
- Add memory proxies tab index
|
||||||
|
|
||||||
|
- Support hidden group
|
||||||
|
|
||||||
|
- Optimize logs
|
||||||
|
|
||||||
|
- Fix externalController hot load error
|
||||||
|
|
||||||
|
## v0.8.21
|
||||||
|
|
||||||
|
- Add tcp concurrent switch
|
||||||
|
|
||||||
|
- Add system proxy switch
|
||||||
|
|
||||||
|
- Add geodata loader switch
|
||||||
|
|
||||||
|
- Add external controller switch
|
||||||
|
|
||||||
|
- Add auto gc on trim memory
|
||||||
|
|
||||||
|
- Fix android notification error
|
||||||
|
|
||||||
|
## v0.8.20
|
||||||
|
|
||||||
|
- Fix ipv6 error
|
||||||
|
|
||||||
|
- Fix android udp direct error
|
||||||
|
|
||||||
|
- Add ipv6 switch
|
||||||
|
|
||||||
|
- Add access all selected button
|
||||||
|
|
||||||
|
- Remove android low version splash
|
||||||
|
|
||||||
|
## v0.8.19
|
||||||
|
|
||||||
|
- Update version
|
||||||
|
|
||||||
|
- Add allowBypass
|
||||||
|
|
||||||
|
- Fix Android only pick .text file issues
|
||||||
|
|
||||||
|
## v0.8.18
|
||||||
|
|
||||||
|
- Fix search issues
|
||||||
|
|
||||||
|
## v0.8.17
|
||||||
|
|
||||||
|
- Fix LoadBalance, Relay load error
|
||||||
|
|
||||||
|
- Fix build.yml4
|
||||||
|
|
||||||
|
- Fix build.yml3
|
||||||
|
|
||||||
|
- Fix build.yml2
|
||||||
|
|
||||||
|
- Fix build.yml
|
||||||
|
|
||||||
|
- Add search function at access control
|
||||||
|
|
||||||
|
- Fix the issues with the profile add button to cover the edit button
|
||||||
|
|
||||||
|
- Adapt LoadBalance and Relay
|
||||||
|
|
||||||
|
- Add arm
|
||||||
|
|
||||||
|
- Fix android notification icon error
|
||||||
|
|
||||||
|
## v0.8.16
|
||||||
|
|
||||||
|
- Add one-click update all profiles
|
||||||
|
- Add expire show
|
||||||
|
|
||||||
|
## v0.8.15
|
||||||
|
|
||||||
|
- Temp remove tun mode
|
||||||
|
|
||||||
|
- Remove macos in workflow
|
||||||
|
|
||||||
|
- Change go version
|
||||||
|
|
||||||
|
## v0.8.14
|
||||||
|
|
||||||
|
- Update Version
|
||||||
|
|
||||||
|
- Fix tun unable to open
|
||||||
|
|
||||||
|
## v0.8.13
|
||||||
|
|
||||||
|
- Optimize delay test2
|
||||||
|
|
||||||
|
- Optimize delay test
|
||||||
|
|
||||||
|
- Add check ip
|
||||||
|
|
||||||
|
- add check ip request
|
||||||
|
|
||||||
|
## v0.8.12
|
||||||
|
|
||||||
|
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
|
||||||
|
|
||||||
|
- Fix edit profile error
|
||||||
|
|
||||||
|
- Fix quickStart change proxy error
|
||||||
|
|
||||||
|
- Fix core version
|
||||||
|
|
||||||
|
## v0.8.10
|
||||||
|
|
||||||
|
- Fix core version
|
||||||
|
|
||||||
|
## v0.8.9
|
||||||
|
|
||||||
|
- Update file_picker
|
||||||
|
|
||||||
|
- Add resources page
|
||||||
|
|
||||||
|
- Optimize more detail
|
||||||
|
|
||||||
|
- Add access selected sorted
|
||||||
|
|
||||||
|
- Fix notification duplicate creation issue
|
||||||
|
|
||||||
|
- Fix AccessControl click issue
|
||||||
|
|
||||||
|
## v0.8.7
|
||||||
|
|
||||||
|
- Fix Workflow
|
||||||
|
|
||||||
|
- Fix Linux unable to open
|
||||||
|
|
||||||
|
- Update README.md 3
|
||||||
|
|
||||||
|
- Create LICENSE
|
||||||
|
- Update README.md 2
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
- Optimize workFlow
|
||||||
|
|
||||||
|
## v0.8.6
|
||||||
|
|
||||||
|
- optimize checkUpdate
|
||||||
|
|
||||||
|
## v0.8.5
|
||||||
|
|
||||||
|
- Fix submit error
|
||||||
|
|
||||||
|
## v0.8.4
|
||||||
|
|
||||||
|
- add WebDAV
|
||||||
|
|
||||||
|
- add Auto check updates
|
||||||
|
|
||||||
|
- Optimize more details
|
||||||
|
|
||||||
|
- optimize delayTest
|
||||||
|
|
||||||
|
## v0.8.2
|
||||||
|
|
||||||
|
- upgrade flutter version
|
||||||
|
|
||||||
|
## v0.8.1
|
||||||
|
|
||||||
|
- Update kernel
|
||||||
|
- Add import profile via QR code image
|
||||||
|
|
||||||
|
## v0.8.0
|
||||||
|
|
||||||
|
- Add compatibility mode and adapt clash scheme.
|
||||||
|
|
||||||
|
## v0.7.14
|
||||||
|
|
||||||
|
- update Version
|
||||||
|
|
||||||
|
- Reconstruction application proxy logic
|
||||||
|
|
||||||
|
## v0.7.13
|
||||||
|
|
||||||
|
- Fix Tab destroy error
|
||||||
|
|
||||||
|
## v0.7.12
|
||||||
|
|
||||||
|
- Optimize repeat healthcheck
|
||||||
|
|
||||||
|
## v0.7.11
|
||||||
|
|
||||||
|
- Optimize Direct mode ui
|
||||||
|
|
||||||
|
## v0.7.10
|
||||||
|
|
||||||
|
- Optimize Healthcheck
|
||||||
|
|
||||||
|
- Remove proxies position animation, improve performance
|
||||||
|
- Add Telegram Link
|
||||||
|
|
||||||
|
- Update healthcheck policy
|
||||||
|
|
||||||
|
- New Check URLTest
|
||||||
|
|
||||||
|
- Fix the problem of invalid auto-selection
|
||||||
|
|
||||||
|
## v0.7.8
|
||||||
|
|
||||||
|
- New Async UpdateConfig
|
||||||
|
|
||||||
|
- add changeProfileDebounce
|
||||||
|
|
||||||
|
- Update Workflow
|
||||||
|
|
||||||
|
- Fix ChangeProfile block
|
||||||
|
|
||||||
|
- Fix Release Message Error
|
||||||
|
|
||||||
|
## v0.7.7
|
||||||
|
|
||||||
|
- Update Selector 2
|
||||||
|
|
||||||
|
## v0.7.6
|
||||||
|
|
||||||
|
- Update Version
|
||||||
|
|
||||||
|
- Fix Proxies Select Error
|
||||||
|
|
||||||
|
## v0.7.5
|
||||||
|
|
||||||
|
- Fix the problem that the proxy group is empty in global mode.
|
||||||
|
|
||||||
|
- Fix the problem that the proxy group is empty in global mode.
|
||||||
|
|
||||||
|
## v0.7.4
|
||||||
|
|
||||||
|
- Add ProxyProvider2
|
||||||
|
|
||||||
|
## v0.7.3
|
||||||
|
|
||||||
|
- Add ProxyProvider
|
||||||
|
|
||||||
|
- Update Version
|
||||||
|
|
||||||
|
- Update ProxyGroup Sort
|
||||||
|
|
||||||
|
- Fix Android quickStart VpnService some problems
|
||||||
|
|
||||||
|
## v0.7.1
|
||||||
|
|
||||||
|
- Update version
|
||||||
|
|
||||||
|
- Set Android notification low importance
|
||||||
|
|
||||||
|
- Fix the issue that VpnService can't be closed correctly in special cases
|
||||||
|
|
||||||
|
- Fix the problem that TileService is not destroyed correctly in some cases
|
||||||
|
|
||||||
|
- Adjust tab animation defaults
|
||||||
|
|
||||||
|
- Add Telegram in README_zh_CN.md
|
||||||
|
|
||||||
|
- Add Telegram
|
||||||
|
|
||||||
|
## v0.7.0
|
||||||
|
|
||||||
|
- update mobile_scanner
|
||||||
|
|
||||||
|
- Initial commit
|
||||||
|
|
||||||
17
README.md
17
README.md
@@ -6,13 +6,9 @@
|
|||||||
|
|
||||||
## FlClash
|
## FlClash
|
||||||
|
|
||||||
<p style="text-align: left;">
|
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||||
<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"/>
|
[](https://t.me/FlClash)
|
||||||
<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.
|
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||||
|
|
||||||
@@ -42,10 +38,6 @@ on Mobile:
|
|||||||
|
|
||||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
<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
|
## Build
|
||||||
|
|
||||||
1. Update submodules
|
1. Update submodules
|
||||||
@@ -100,9 +92,6 @@ on Mobile:
|
|||||||
```bash
|
```bash
|
||||||
dart .\setup.dart
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Star
|
## Star
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,10 @@
|
|||||||
|
|
||||||
## FlClash
|
## FlClash
|
||||||
|
|
||||||
<p style="text-align: left;">
|
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||||
<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"/>
|
[](https://t.me/FlClash)
|
||||||
<a href="LICENSE">
|
|
||||||
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
||||||
|
|
||||||
@@ -42,11 +39,6 @@ on Mobile:
|
|||||||
|
|
||||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
<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
|
## Build
|
||||||
|
|
||||||
1. 更新 submodules
|
1. 更新 submodules
|
||||||
|
|||||||
@@ -34,22 +34,22 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
|
|||||||
android {
|
android {
|
||||||
namespace "com.follow.clash"
|
namespace "com.follow.clash"
|
||||||
compileSdkVersion 34
|
compileSdkVersion 34
|
||||||
ndkVersion "25.1.8937393"
|
ndkVersion "27.1.12297006"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '17'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (isRelease){
|
if (isRelease) {
|
||||||
release {
|
release {
|
||||||
storeFile defStoreFile
|
storeFile defStoreFile
|
||||||
storePassword defStorePassword
|
storePassword defStorePassword
|
||||||
@@ -74,10 +74,9 @@ android {
|
|||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
if (isRelease) {
|
||||||
if(isRelease){
|
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}else{
|
} else {
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
|
|||||||
@@ -10,25 +10,22 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<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.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
|
||||||
tools:ignore="SystemPermissionTypo" />
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="FlClash"
|
android:hardwareAccelerated="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:label="FlClash">
|
||||||
tools:targetApi="tiramisu">
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.follow.clash.MainActivity"
|
android:name="com.follow.clash.MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
@@ -72,7 +69,17 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".TempActivity"
|
android:name=".TempActivity"
|
||||||
android:theme="@style/TransparentTheme" />
|
android:exported="true"
|
||||||
|
android:theme="@style/TransparentTheme">
|
||||||
|
<intent-filter>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<action android:name="com.follow.clash.action.START" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<action android:name="com.follow.clash.action.STOP" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.FlClashTileService"
|
android:name=".services.FlClashTileService"
|
||||||
@@ -119,12 +126,19 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="vpn" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.FlClashService"
|
android:name=".services.FlClashService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse" />
|
android:foregroundServiceType="specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="service" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.follow.clash
|
package com.follow.clash
|
||||||
|
|
||||||
import com.follow.clash.models.Props
|
|
||||||
|
import com.follow.clash.models.VpnOptions
|
||||||
|
|
||||||
interface BaseServiceInterface {
|
interface BaseServiceInterface {
|
||||||
fun start(port: Int, props: Props?): Int?
|
fun start(options: VpnOptions): Int
|
||||||
fun stop()
|
fun stop()
|
||||||
fun startForeground(title: String, content: String)
|
fun startForeground(title: String, content: String)
|
||||||
}
|
}
|
||||||
@@ -33,14 +33,13 @@ object GlobalState {
|
|||||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentTitlePlugin(): TilePlugin? {
|
fun getCurrentTilePlugin(): TilePlugin? {
|
||||||
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
||||||
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
|
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentVPNPlugin(): VpnPlugin? {
|
fun getCurrentVPNPlugin(): VpnPlugin? {
|
||||||
val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
|
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||||
return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroyServiceEngine() {
|
fun destroyServiceEngine() {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.follow.clash
|
package com.follow.clash
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import com.follow.clash.plugins.AppPlugin
|
import com.follow.clash.plugins.AppPlugin
|
||||||
import com.follow.clash.plugins.ServicePlugin
|
import com.follow.clash.plugins.ServicePlugin
|
||||||
import com.follow.clash.plugins.VpnPlugin
|
import com.follow.clash.plugins.VpnPlugin
|
||||||
@@ -9,7 +11,6 @@ import io.flutter.embedding.android.FlutterActivity
|
|||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.plugins.add(AppPlugin())
|
flutterEngine.plugins.add(AppPlugin())
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import android.os.Bundle
|
|||||||
class TempActivity : Activity() {
|
class TempActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
when (intent.action) {
|
||||||
|
"com.follow.clash.action.START" -> {
|
||||||
|
GlobalState.getCurrentTilePlugin()?.handleStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
"com.follow.clash.action.STOP" -> {
|
||||||
|
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,21 @@
|
|||||||
package com.follow.clash.extensions
|
package com.follow.clash.extensions
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
import android.system.OsConstants.IPPROTO_TCP
|
import android.system.OsConstants.IPPROTO_TCP
|
||||||
import android.system.OsConstants.IPPROTO_UDP
|
import android.system.OsConstants.IPPROTO_UDP
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.follow.clash.MainActivity
|
import com.follow.clash.models.CIDR
|
||||||
import com.follow.clash.R
|
|
||||||
import com.follow.clash.models.Metadata
|
import com.follow.clash.models.Metadata
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
|
||||||
suspend fun Drawable.getBase64(): String {
|
suspend fun Drawable.getBase64(): String {
|
||||||
@@ -41,6 +34,56 @@ fun Metadata.getProtocol(): Int? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val CHANNEL = "FlClash"
|
fun String.toCIDR(): CIDR {
|
||||||
|
val parts = split("/")
|
||||||
|
if (parts.size != 2) {
|
||||||
|
throw IllegalArgumentException("Invalid CIDR format")
|
||||||
|
}
|
||||||
|
val ipAddress = parts[0]
|
||||||
|
val prefixLength = parts[1].toIntOrNull()
|
||||||
|
?: throw IllegalArgumentException("Invalid prefix length")
|
||||||
|
|
||||||
private val notificationId: Int = 1
|
val address = InetAddress.getByName(ipAddress)
|
||||||
|
|
||||||
|
val maxPrefix = if (address.address.size == 4) 32 else 128
|
||||||
|
if (prefixLength < 0 || prefixLength > maxPrefix) {
|
||||||
|
throw IllegalArgumentException("Invalid prefix length for IP version")
|
||||||
|
}
|
||||||
|
|
||||||
|
return CIDR(address, prefixLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||||
|
val properties = getLinkProperties(network) ?: return listOf()
|
||||||
|
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InetAddress.asSocketAddressText(port: Int): String {
|
||||||
|
return when (this) {
|
||||||
|
is Inet6Address ->
|
||||||
|
"[${numericToTextFormat(this.address)}]:$port"
|
||||||
|
|
||||||
|
is Inet4Address ->
|
||||||
|
"${this.hostAddress}:$port"
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun numericToTextFormat(src: ByteArray): String {
|
||||||
|
val sb = StringBuilder(39)
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
sb.append(
|
||||||
|
Integer.toHexString(
|
||||||
|
src[i shl 1].toInt() shl 8 and 0xff00
|
||||||
|
or (src[(i shl 1) + 1].toInt() and 0xff)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (i < 7) {
|
||||||
|
sb.append(":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.follow.clash.models
|
package com.follow.clash.models
|
||||||
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
data class Package(
|
data class Package(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val label: String,
|
val label: String,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.follow.clash.models
|
package com.follow.clash.models
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
enum class AccessControlMode {
|
enum class AccessControlMode {
|
||||||
acceptSelected,
|
acceptSelected,
|
||||||
rejectSelected,
|
rejectSelected,
|
||||||
@@ -11,9 +13,16 @@ data class AccessControl(
|
|||||||
val rejectList: List<String>,
|
val rejectList: List<String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Props(
|
data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||||
val enable: Boolean?,
|
|
||||||
|
data class VpnOptions(
|
||||||
|
val enable: Boolean,
|
||||||
|
val port: Int,
|
||||||
val accessControl: AccessControl?,
|
val accessControl: AccessControl?,
|
||||||
val allowBypass: Boolean?,
|
val allowBypass: Boolean,
|
||||||
val systemProxy: Boolean?,
|
val systemProxy: Boolean,
|
||||||
)
|
val bypassDomain: List<String>,
|
||||||
|
val ipv4Address: String,
|
||||||
|
val ipv6Address: String,
|
||||||
|
val dnsServerAddress: String,
|
||||||
|
)
|
||||||
@@ -8,7 +8,6 @@ import android.content.Intent
|
|||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.ComponentInfo
|
import android.content.pm.ComponentInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -17,12 +16,9 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.extensions.getBase64
|
import com.follow.clash.extensions.getBase64
|
||||||
import com.follow.clash.extensions.getProtocol
|
|
||||||
import com.follow.clash.models.Package
|
import com.follow.clash.models.Package
|
||||||
import com.follow.clash.models.Process
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
@@ -37,7 +33,6 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||||
@@ -52,11 +47,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
private lateinit var scope: CoroutineScope
|
private lateinit var scope: CoroutineScope
|
||||||
|
|
||||||
private var connectivity: ConnectivityManager? = null
|
|
||||||
|
|
||||||
private var vpnCallBack: (() -> Unit)? = null
|
private var vpnCallBack: (() -> Unit)? = null
|
||||||
|
|
||||||
private val iconMap = mutableMapOf<String, String?>()
|
private val iconMap = mutableMapOf<String, String?>()
|
||||||
|
|
||||||
private val packages = mutableListOf<Package>()
|
private val packages = mutableListOf<Package>()
|
||||||
|
|
||||||
private val skipPrefixList = listOf(
|
private val skipPrefixList = listOf(
|
||||||
@@ -114,7 +108,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||||
|
|
||||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||||
@@ -191,48 +184,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"resolverProcess" -> {
|
|
||||||
val data = call.argument<String>("data")
|
|
||||||
val process =
|
|
||||||
if (data != null) Gson().fromJson(
|
|
||||||
data,
|
|
||||||
Process::class.java
|
|
||||||
) else null
|
|
||||||
val metadata = process?.metadata
|
|
||||||
val protocol = metadata?.getProtocol()
|
|
||||||
if (protocol == null) {
|
|
||||||
result.success(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
result.success(null)
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
if (connectivity == null) {
|
|
||||||
connectivity = context.getSystemService<ConnectivityManager>()
|
|
||||||
}
|
|
||||||
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
|
||||||
val dst = InetSocketAddress(
|
|
||||||
metadata.destinationIP.ifEmpty { metadata.host },
|
|
||||||
metadata.destinationPort
|
|
||||||
)
|
|
||||||
val uid = try {
|
|
||||||
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (uid == null || uid == -1) {
|
|
||||||
result.success(null)
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
val packages = context.packageManager?.getPackagesForUid(uid)
|
|
||||||
result.success(packages?.first())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"tip" -> {
|
"tip" -> {
|
||||||
val message = call.argument<String>("message")
|
val message = call.argument<String>("message")
|
||||||
tip(message)
|
tip(message)
|
||||||
@@ -379,7 +330,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun isChinaPackage(packageName: String): Boolean {
|
private fun isChinaPackage(packageName: String): Boolean {
|
||||||
val packageManager = context.packageManager ?: return false
|
val packageManager = context.packageManager ?: return false
|
||||||
skipPrefixList.forEach {
|
skipPrefixList.forEach {
|
||||||
@@ -447,10 +397,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestGc() {
|
|
||||||
channel.invokeMethod("gc", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity;
|
activity = binding.activity;
|
||||||
binding.addActivityResultListener(::onActivityResult)
|
binding.addActivityResultListener(::onActivityResult)
|
||||||
@@ -490,4 +436,4 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.follow.clash.plugins
|
package com.follow.clash.plugins
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
|||||||
@@ -1,31 +1,47 @@
|
|||||||
package com.follow.clash.plugins
|
package com.follow.clash.plugins
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import androidx.core.content.getSystemService
|
||||||
import com.follow.clash.BaseServiceInterface
|
import com.follow.clash.BaseServiceInterface
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.RunState
|
import com.follow.clash.RunState
|
||||||
import com.follow.clash.models.Props
|
import com.follow.clash.extensions.getProtocol
|
||||||
|
import com.follow.clash.extensions.resolveDns
|
||||||
import com.follow.clash.services.FlClashService
|
import com.follow.clash.services.FlClashService
|
||||||
import com.follow.clash.services.FlClashVpnService
|
import com.follow.clash.services.FlClashVpnService
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
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 kotlin.concurrent.withLock
|
||||||
|
import com.follow.clash.models.Process
|
||||||
|
import com.follow.clash.models.VpnOptions
|
||||||
|
|
||||||
|
|
||||||
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||||
private lateinit var flutterMethodChannel: MethodChannel
|
private lateinit var flutterMethodChannel: MethodChannel
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
private var flClashService: BaseServiceInterface? = null
|
private var flClashService: BaseServiceInterface? = null
|
||||||
private var port: Int = 7890
|
private lateinit var options: VpnOptions
|
||||||
private var props: Props? = null
|
private lateinit var scope: CoroutineScope
|
||||||
|
|
||||||
|
private val connectivity by lazy {
|
||||||
|
context.getSystemService<ConnectivityManager>()
|
||||||
|
}
|
||||||
|
|
||||||
private val connection = object : ServiceConnection {
|
private val connection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
@@ -43,66 +59,169 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
scope = CoroutineScope(Dispatchers.Default)
|
||||||
context = flutterPluginBinding.applicationContext
|
context = flutterPluginBinding.applicationContext
|
||||||
|
scope.launch {
|
||||||
|
registerNetworkCallback()
|
||||||
|
}
|
||||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
|
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
|
||||||
flutterMethodChannel.setMethodCallHandler(this)
|
flutterMethodChannel.setMethodCallHandler(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
unRegisterNetworkCallback()
|
||||||
flutterMethodChannel.setMethodCallHandler(null)
|
flutterMethodChannel.setMethodCallHandler(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
"start" -> {
|
"start" -> {
|
||||||
port = call.argument<Int>("port")!!
|
val data = call.argument<String>("data")
|
||||||
val args = call.argument<String>("args")
|
options = Gson().fromJson(data, VpnOptions::class.java)
|
||||||
props =
|
when (options.enable) {
|
||||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
true -> handleStartVpn()
|
||||||
when (props?.enable == true) {
|
false -> start()
|
||||||
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)
|
result.success(true)
|
||||||
} else {
|
|
||||||
result.success(false)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
"startForeground" -> {
|
"stop" -> {
|
||||||
val title = call.argument<String>("title") as String
|
stop()
|
||||||
val content = call.argument<String>("content") as String
|
result.success(true)
|
||||||
startForeground(title, content)
|
}
|
||||||
result.success(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
"setProtect" -> {
|
||||||
result.notImplemented()
|
val fd = call.argument<Int>("fd")
|
||||||
|
if (fd != null) {
|
||||||
|
if (flClashService is FlClashVpnService) {
|
||||||
|
(flClashService as FlClashVpnService).protect(fd)
|
||||||
|
}
|
||||||
|
result.success(true)
|
||||||
|
} else {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"startForeground" -> {
|
||||||
|
val title = call.argument<String>("title") as String
|
||||||
|
val content = call.argument<String>("content") as String
|
||||||
|
startForeground(title, content)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
"resolverProcess" -> {
|
||||||
|
val data = call.argument<String>("data")
|
||||||
|
val process =
|
||||||
|
if (data != null) Gson().fromJson(
|
||||||
|
data,
|
||||||
|
Process::class.java
|
||||||
|
) else null
|
||||||
|
val metadata = process?.metadata
|
||||||
|
if (metadata == null) {
|
||||||
|
result.success(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val protocol = metadata.getProtocol()
|
||||||
|
if (protocol == null) {
|
||||||
|
result.success(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
result.success(null)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
|
||||||
|
val dst = InetSocketAddress(
|
||||||
|
metadata.destinationIP.ifEmpty { metadata.host },
|
||||||
|
metadata.destinationPort
|
||||||
|
)
|
||||||
|
val uid = try {
|
||||||
|
connectivity?.getConnectionOwnerUid(protocol, src, dst)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (uid == null || uid == -1) {
|
||||||
|
result.success(null)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val packages = context.packageManager?.getPackagesForUid(uid)
|
||||||
|
result.success(packages?.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ForegroundServiceType")
|
private fun handleStartVpn() {
|
||||||
fun handleStartVpn() {
|
|
||||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ForegroundServiceType")
|
fun requestGc() {
|
||||||
|
flutterMethodChannel.invokeMethod("gc", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val networks = mutableSetOf<Network>()
|
||||||
|
|
||||||
|
fun onUpdateNetwork() {
|
||||||
|
val dns = networks.flatMap { network ->
|
||||||
|
connectivity?.resolveDns(network) ?: emptyList()
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
.joinToString(",")
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (flClashService is FlClashVpnService) {
|
||||||
|
// val network = networks.maxByOrNull { net ->
|
||||||
|
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
|
||||||
|
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
|
||||||
|
// } ?: -1
|
||||||
|
// }
|
||||||
|
// network?.let {
|
||||||
|
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
networks.add(network)
|
||||||
|
onUpdateNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
networks.remove(network)
|
||||||
|
onUpdateNetwork()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val request = NetworkRequest.Builder().apply {
|
||||||
|
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
|
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
private fun registerNetworkCallback() {
|
||||||
|
networks.clear()
|
||||||
|
connectivity?.registerNetworkCallback(request, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unRegisterNetworkCallback() {
|
||||||
|
connectivity?.unregisterNetworkCallback(callback)
|
||||||
|
networks.clear()
|
||||||
|
onUpdateNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
private fun startForeground(title: String, content: String) {
|
private fun startForeground(title: String, content: String) {
|
||||||
GlobalState.runLock.withLock {
|
GlobalState.runLock.withLock {
|
||||||
if (GlobalState.runState.value != RunState.START) return
|
if (GlobalState.runState.value != RunState.START) return
|
||||||
@@ -118,8 +237,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
GlobalState.runLock.withLock {
|
GlobalState.runLock.withLock {
|
||||||
if (GlobalState.runState.value == RunState.START) return
|
if (GlobalState.runState.value == RunState.START) return
|
||||||
GlobalState.runState.value = RunState.START
|
GlobalState.runState.value = RunState.START
|
||||||
val fd = flClashService?.start(port, props)
|
val fd = flClashService?.start(options)
|
||||||
flutterMethodChannel.invokeMethod("started", fd)
|
flutterMethodChannel.invokeMethod(
|
||||||
|
"started",
|
||||||
|
fd
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +255,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
val intent = when (props?.enable == true) {
|
val intent = when (options.enable) {
|
||||||
true -> Intent(context, FlClashVpnService::class.java)
|
true -> Intent(context, FlClashVpnService::class.java)
|
||||||
false -> Intent(context, FlClashService::class.java)
|
false -> Intent(context, FlClashService::class.java)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.follow.clash.BaseServiceInterface
|
import com.follow.clash.BaseServiceInterface
|
||||||
import com.follow.clash.MainActivity
|
import com.follow.clash.MainActivity
|
||||||
import com.follow.clash.models.Props
|
import com.follow.clash.models.VpnOptions
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
class FlClashService : Service(), BaseServiceInterface {
|
class FlClashService : Service(), BaseServiceInterface {
|
||||||
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
@@ -73,7 +71,7 @@ class FlClashService : Service(), BaseServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start(port: Int, props: Props?): Int? = null
|
override fun start(options: VpnOptions) = 0
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.follow.clash.services
|
package com.follow.clash.services
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -37,6 +38,7 @@ class FlClashTileService : TileService() {
|
|||||||
GlobalState.runState.observeForever(observer)
|
GlobalState.runState.observeForever(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||||
private fun activityTransfer() {
|
private fun activityTransfer() {
|
||||||
val intent = Intent(this, TempActivity::class.java)
|
val intent = Intent(this, TempActivity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||||
@@ -67,15 +69,15 @@ class FlClashTileService : TileService() {
|
|||||||
activityTransfer()
|
activityTransfer()
|
||||||
if (GlobalState.runState.value == RunState.STOP) {
|
if (GlobalState.runState.value == RunState.STOP) {
|
||||||
GlobalState.runState.value = RunState.PENDING
|
GlobalState.runState.value = RunState.PENDING
|
||||||
val titlePlugin = GlobalState.getCurrentTitlePlugin()
|
val tilePlugin = GlobalState.getCurrentTilePlugin()
|
||||||
if (titlePlugin != null) {
|
if (tilePlugin != null) {
|
||||||
titlePlugin.handleStart()
|
tilePlugin.handleStart()
|
||||||
} else {
|
} else {
|
||||||
GlobalState.initServiceEngine(applicationContext)
|
GlobalState.initServiceEngine(applicationContext)
|
||||||
}
|
}
|
||||||
} else if (GlobalState.runState.value == RunState.START) {
|
} else if (GlobalState.runState.value == RunState.START) {
|
||||||
GlobalState.runState.value = RunState.PENDING
|
GlobalState.runState.value = RunState.PENDING
|
||||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
|
import android.net.Network
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
@@ -14,53 +15,41 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.follow.clash.BaseServiceInterface
|
import com.follow.clash.BaseServiceInterface
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.MainActivity
|
import com.follow.clash.MainActivity
|
||||||
import com.follow.clash.R
|
import com.follow.clash.R
|
||||||
|
import com.follow.clash.TempActivity
|
||||||
|
import com.follow.clash.extensions.toCIDR
|
||||||
import com.follow.clash.models.AccessControlMode
|
import com.follow.clash.models.AccessControlMode
|
||||||
import com.follow.clash.models.Props
|
import com.follow.clash.models.VpnOptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
|
||||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||||
|
|
||||||
private val passList = listOf(
|
|
||||||
"*zhihu.com",
|
|
||||||
"*zhimg.com",
|
|
||||||
"*jd.com",
|
|
||||||
"100ime-iat-api.xfyun.cn",
|
|
||||||
"*360buyimg.com",
|
|
||||||
"localhost",
|
|
||||||
"*.local",
|
|
||||||
"127.*",
|
|
||||||
"10.*",
|
|
||||||
"172.16.*",
|
|
||||||
"172.17.*",
|
|
||||||
"172.18.*",
|
|
||||||
"172.19.*",
|
|
||||||
"172.2*",
|
|
||||||
"172.30.*",
|
|
||||||
"172.31.*",
|
|
||||||
"192.168.*"
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
GlobalState.initServiceEngine(applicationContext)
|
GlobalState.initServiceEngine(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start(port: Int, props: Props?): Int? {
|
override fun start(options: VpnOptions): Int {
|
||||||
return with(Builder()) {
|
return with(Builder()) {
|
||||||
addAddress("172.16.0.1", 30)
|
if (options.ipv4Address.isNotEmpty()) {
|
||||||
|
val cidr = options.ipv4Address.toCIDR()
|
||||||
|
addAddress(cidr.address, cidr.prefixLength)
|
||||||
|
addRoute("0.0.0.0", 0)
|
||||||
|
}
|
||||||
|
if (options.ipv6Address.isNotEmpty()) {
|
||||||
|
val cidr = options.ipv6Address.toCIDR()
|
||||||
|
addAddress(cidr.address, cidr.prefixLength)
|
||||||
|
addRoute("::", 0)
|
||||||
|
}
|
||||||
|
addDnsServer(options.dnsServerAddress)
|
||||||
setMtu(9000)
|
setMtu(9000)
|
||||||
addRoute("0.0.0.0", 0)
|
options.accessControl?.let { accessControl ->
|
||||||
props?.accessControl?.let { accessControl ->
|
|
||||||
when (accessControl.mode) {
|
when (accessControl.mode) {
|
||||||
AccessControlMode.acceptSelected -> {
|
AccessControlMode.acceptSelected -> {
|
||||||
(accessControl.acceptList + packageName).forEach {
|
(accessControl.acceptList + packageName).forEach {
|
||||||
@@ -75,28 +64,33 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addDnsServer("172.16.0.2")
|
|
||||||
setSession("FlClash")
|
setSession("FlClash")
|
||||||
setBlocking(false)
|
setBlocking(false)
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
setMetered(false)
|
setMetered(false)
|
||||||
}
|
}
|
||||||
if (props?.allowBypass == true) {
|
if (options.allowBypass) {
|
||||||
allowBypass()
|
allowBypass()
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
|
||||||
setHttpProxy(
|
setHttpProxy(
|
||||||
ProxyInfo.buildDirectProxy(
|
ProxyInfo.buildDirectProxy(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
port,
|
options.port,
|
||||||
passList
|
options.bypassDomain
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
establish()?.detachFd()
|
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() {
|
override fun stop() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
@@ -127,6 +121,27 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val stopIntent = Intent(this, TempActivity::class.java)
|
||||||
|
stopIntent.action = "com.follow.clash.action.STOP"
|
||||||
|
stopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||||
|
|
||||||
|
|
||||||
|
val stopPendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
stopIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
stopIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||||
setSmallIcon(R.drawable.ic_stat_name)
|
setSmallIcon(R.drawable.ic_stat_name)
|
||||||
setContentTitle("FlClash")
|
setContentTitle("FlClash")
|
||||||
@@ -140,6 +155,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
|
addAction(0, "Stop", stopPendingIntent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +181,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
super.onTrimMemory(level)
|
super.onTrimMemory(level)
|
||||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
GlobalState.getCurrentVPNPlugin()?.requestGc()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
@@ -178,7 +194,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
val isSuccess = super.onTransact(code, data, reply, flags)
|
val isSuccess = super.onTransact(code, data, reply, flags)
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isSuccess
|
return isSuccess
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
64257
assets/data/GeoSite.dat
64257
assets/data/GeoSite.dat
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
assets/images/icon_black.ico
Normal file
BIN
assets/images/icon_black.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/images/icon_black.png
Normal file
BIN
assets/images/icon_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/images/icon_white.ico
Normal file
BIN
assets/images/icon_white.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Submodule core/Clash.Meta updated: a61c926a17...e89569916a
309
core/common.go
309
core/common.go
@@ -3,14 +3,15 @@ package main
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"core/state"
|
||||||
"errors"
|
"errors"
|
||||||
route "github.com/metacubex/mihomo/hub/route"
|
"github.com/metacubex/mihomo/constant/features"
|
||||||
|
"github.com/metacubex/mihomo/hub/route"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -234,151 +235,151 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
|
|||||||
return prof
|
return prof
|
||||||
}
|
}
|
||||||
|
|
||||||
func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
|
//func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
|
||||||
for _, v := range s {
|
// for _, v := range s {
|
||||||
initVal = f(initVal, v)
|
// initVal = f(initVal, v)
|
||||||
}
|
// }
|
||||||
return initVal
|
// return initVal
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
|
//func Map[T, U any](slice []T, fn func(T) U) []U {
|
||||||
|
// result := make([]U, len(slice))
|
||||||
|
// for i, v := range slice {
|
||||||
|
// result[i] = fn(v)
|
||||||
|
// }
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func replaceFromMap(s string, m map[string]string) string {
|
||||||
|
// for k, v := range m {
|
||||||
|
// s = strings.ReplaceAll(s, k, v)
|
||||||
|
// }
|
||||||
|
// return s
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func removeDuplicateFromSlice[T any](slice []T) []T {
|
||||||
|
// result := make([]T, 0)
|
||||||
|
// seen := make(map[any]struct{})
|
||||||
|
// for _, value := range slice {
|
||||||
|
// if _, ok := seen[value]; !ok {
|
||||||
|
// result = append(result, value)
|
||||||
|
// seen[value] = struct{}{}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
|
|
||||||
func Map[T, U any](slice []T, fn func(T) U) []U {
|
//func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
|
||||||
result := make([]U, len(slice))
|
// var replacements = map[string]string{}
|
||||||
for i, v := range slice {
|
// var selectArr []map[string]any
|
||||||
result[i] = fn(v)
|
// var urlTestArr []map[string]any
|
||||||
}
|
// var fallbackArr []map[string]any
|
||||||
return result
|
// for _, group := range *proxyGroup {
|
||||||
}
|
// switch group["type"] {
|
||||||
|
// case "select":
|
||||||
func replaceFromMap(s string, m map[string]string) string {
|
// selectArr = append(selectArr, group)
|
||||||
for k, v := range m {
|
// replacements[group["name"].(string)] = "Proxy"
|
||||||
s = strings.ReplaceAll(s, k, v)
|
// break
|
||||||
}
|
// case "url-test":
|
||||||
return s
|
// urlTestArr = append(urlTestArr, group)
|
||||||
}
|
// replacements[group["name"].(string)] = "Auto"
|
||||||
|
// break
|
||||||
func removeDuplicateFromSlice[T any](slice []T) []T {
|
// case "fallback":
|
||||||
result := make([]T, 0)
|
// fallbackArr = append(fallbackArr, group)
|
||||||
seen := make(map[any]struct{})
|
// replacements[group["name"].(string)] = "Fallback"
|
||||||
for _, value := range slice {
|
// break
|
||||||
if _, ok := seen[value]; !ok {
|
// default:
|
||||||
result = append(result, value)
|
// break
|
||||||
seen[value] = struct{}{}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
return result
|
// ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||||
}
|
// if cur["proxies"] == nil {
|
||||||
|
// return res
|
||||||
func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
|
// }
|
||||||
var replacements = map[string]string{}
|
// for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||||
var selectArr []map[string]any
|
// if str, ok := proxyName.(string); ok {
|
||||||
var urlTestArr []map[string]any
|
// str = replaceFromMap(str, replacements)
|
||||||
var fallbackArr []map[string]any
|
// if str != "Proxy" {
|
||||||
for _, group := range *proxyGroup {
|
// res = append(res, str)
|
||||||
switch group["type"] {
|
// }
|
||||||
case "select":
|
// }
|
||||||
selectArr = append(selectArr, group)
|
// }
|
||||||
replacements[group["name"].(string)] = "Proxy"
|
// return res
|
||||||
break
|
// })
|
||||||
case "url-test":
|
//
|
||||||
urlTestArr = append(urlTestArr, group)
|
// ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
|
||||||
replacements[group["name"].(string)] = "Auto"
|
//
|
||||||
break
|
// AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||||
case "fallback":
|
// if cur["proxies"] == nil {
|
||||||
fallbackArr = append(fallbackArr, group)
|
// return res
|
||||||
replacements[group["name"].(string)] = "Fallback"
|
// }
|
||||||
break
|
// for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||||
default:
|
// if str, ok := proxyName.(string); ok {
|
||||||
break
|
// str = replaceFromMap(str, replacements)
|
||||||
}
|
// if str != "Auto" {
|
||||||
}
|
// res = append(res, str)
|
||||||
|
// }
|
||||||
ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
|
// }
|
||||||
if cur["proxies"] == nil {
|
// }
|
||||||
return res
|
// return res
|
||||||
}
|
// })
|
||||||
for _, proxyName := range cur["proxies"].([]interface{}) {
|
//
|
||||||
if str, ok := proxyName.(string); ok {
|
// AutoProxies = removeDuplicateFromSlice(AutoProxies)
|
||||||
str = replaceFromMap(str, replacements)
|
//
|
||||||
if str != "Proxy" {
|
// FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||||
res = append(res, str)
|
// if cur["proxies"] == nil {
|
||||||
}
|
// return res
|
||||||
}
|
// }
|
||||||
}
|
// for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||||
return res
|
// if str, ok := proxyName.(string); ok {
|
||||||
})
|
// str = replaceFromMap(str, replacements)
|
||||||
|
// if str != "Fallback" {
|
||||||
ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
|
// res = append(res, str)
|
||||||
|
// }
|
||||||
AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
|
// }
|
||||||
if cur["proxies"] == nil {
|
// }
|
||||||
return res
|
// return res
|
||||||
}
|
// })
|
||||||
for _, proxyName := range cur["proxies"].([]interface{}) {
|
//
|
||||||
if str, ok := proxyName.(string); ok {
|
// FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
|
||||||
str = replaceFromMap(str, replacements)
|
//
|
||||||
if str != "Auto" {
|
// var computedProxyGroup []map[string]any
|
||||||
res = append(res, str)
|
//
|
||||||
}
|
// if len(ProxyProxies) > 0 {
|
||||||
}
|
// computedProxyGroup = append(computedProxyGroup,
|
||||||
}
|
// map[string]any{
|
||||||
return res
|
// "name": "Proxy",
|
||||||
})
|
// "type": "select",
|
||||||
|
// "proxies": ProxyProxies,
|
||||||
AutoProxies = removeDuplicateFromSlice(AutoProxies)
|
// })
|
||||||
|
// }
|
||||||
FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
|
//
|
||||||
if cur["proxies"] == nil {
|
// if len(AutoProxies) > 0 {
|
||||||
return res
|
// computedProxyGroup = append(computedProxyGroup,
|
||||||
}
|
// map[string]any{
|
||||||
for _, proxyName := range cur["proxies"].([]interface{}) {
|
// "name": "Auto",
|
||||||
if str, ok := proxyName.(string); ok {
|
// "type": "url-test",
|
||||||
str = replaceFromMap(str, replacements)
|
// "proxies": AutoProxies,
|
||||||
if str != "Fallback" {
|
// })
|
||||||
res = append(res, str)
|
// }
|
||||||
}
|
//
|
||||||
}
|
// if len(FallbackProxies) > 0 {
|
||||||
}
|
// computedProxyGroup = append(computedProxyGroup,
|
||||||
return res
|
// map[string]any{
|
||||||
})
|
// "name": "Fallback",
|
||||||
|
// "type": "fallback",
|
||||||
FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
|
// "proxies": FallbackProxies,
|
||||||
|
// })
|
||||||
var computedProxyGroup []map[string]any
|
// }
|
||||||
|
//
|
||||||
if len(ProxyProxies) > 0 {
|
// computedRule := Map(*rule, func(value string) string {
|
||||||
computedProxyGroup = append(computedProxyGroup,
|
// return replaceFromMap(value, replacements)
|
||||||
map[string]any{
|
// })
|
||||||
"name": "Proxy",
|
//
|
||||||
"type": "select",
|
// *proxyGroup = computedProxyGroup
|
||||||
"proxies": ProxyProxies,
|
// *rule = computedRule
|
||||||
})
|
//}
|
||||||
}
|
|
||||||
|
|
||||||
if len(AutoProxies) > 0 {
|
|
||||||
computedProxyGroup = append(computedProxyGroup,
|
|
||||||
map[string]any{
|
|
||||||
"name": "Auto",
|
|
||||||
"type": "url-test",
|
|
||||||
"proxies": AutoProxies,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(FallbackProxies) > 0 {
|
|
||||||
computedProxyGroup = append(computedProxyGroup,
|
|
||||||
map[string]any{
|
|
||||||
"name": "Fallback",
|
|
||||||
"type": "fallback",
|
|
||||||
"proxies": FallbackProxies,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
computedRule := Map(*rule, func(value string) string {
|
|
||||||
return replaceFromMap(value, replacements)
|
|
||||||
})
|
|
||||||
|
|
||||||
*proxyGroup = computedProxyGroup
|
|
||||||
*rule = computedRule
|
|
||||||
}
|
|
||||||
|
|
||||||
func genHosts(hosts, patchHosts map[string]any) {
|
func genHosts(hosts, patchHosts map[string]any) {
|
||||||
for k, v := range patchHosts {
|
for k, v := range patchHosts {
|
||||||
@@ -410,6 +411,12 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
|||||||
targetConfig.Profile.StoreSelected = false
|
targetConfig.Profile.StoreSelected = false
|
||||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||||
targetConfig.GlobalUA = patchConfig.GlobalUA
|
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)
|
genHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||||
if configParams.OverrideDns {
|
if configParams.OverrideDns {
|
||||||
targetConfig.DNS = patchConfig.DNS
|
targetConfig.DNS = patchConfig.DNS
|
||||||
@@ -453,7 +460,6 @@ func updateListeners(general *config.General, listeners map[string]constant.Inbo
|
|||||||
}
|
}
|
||||||
runLock.Lock()
|
runLock.Lock()
|
||||||
defer runLock.Unlock()
|
defer runLock.Unlock()
|
||||||
|
|
||||||
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
||||||
listener.SetAllowLan(general.AllowLan)
|
listener.SetAllowLan(general.AllowLan)
|
||||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||||
@@ -468,7 +474,9 @@ func updateListeners(general *config.General, listeners map[string]constant.Inbo
|
|||||||
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
|
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
|
||||||
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
|
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
|
||||||
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
|
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
|
||||||
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
|
if !features.Android {
|
||||||
|
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopListeners() {
|
func stopListeners() {
|
||||||
@@ -522,13 +530,10 @@ func patchSelectGroup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyConfig() error {
|
func applyConfig() error {
|
||||||
cfg, err := config.ParseRawConfig(currentRawConfig)
|
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||||
}
|
}
|
||||||
if configParams.TestURL != nil {
|
|
||||||
constant.DefaultTestURL = *configParams.TestURL
|
|
||||||
}
|
|
||||||
if configParams.IsPatch {
|
if configParams.IsPatch {
|
||||||
patchConfig(cfg.General, cfg.Controller)
|
patchConfig(cfg.General, cfg.Controller)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
20
core/dns.go
Normal file
20
core/dns.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"github.com/metacubex/mihomo/dns"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//export updateDns
|
||||||
|
func updateDns(s *C.char) {
|
||||||
|
dnsList := C.GoString(s)
|
||||||
|
go func() {
|
||||||
|
log.Infoln("[DNS] updateDns %s", dnsList)
|
||||||
|
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
|
||||||
|
dns.FlushCacheWithDefaultResolver()
|
||||||
|
}()
|
||||||
|
}
|
||||||
15
core/go.mod
15
core/go.mod
@@ -4,12 +4,9 @@ go 1.21.0
|
|||||||
|
|
||||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||||
|
|
||||||
require (
|
require github.com/metacubex/mihomo v1.17.1
|
||||||
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
|
|
||||||
github.com/metacubex/mihomo v1.17.1
|
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
|
||||||
github.com/miekg/dns v1.1.62
|
|
||||||
golang.org/x/sync v0.8.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||||
@@ -54,7 +51,7 @@ require (
|
|||||||
github.com/metacubex/chacha v0.1.0 // indirect
|
github.com/metacubex/chacha v0.1.0 // indirect
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // 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/gvisor v0.0.0-20240320004321-933faba989ec // indirect
|
||||||
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58 // indirect
|
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 // indirect
|
||||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // 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-shadowsocks v0.2.8 // indirect
|
||||||
@@ -64,6 +61,7 @@ require (
|
|||||||
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd // indirect
|
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd // indirect
|
||||||
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 // indirect
|
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 // indirect
|
||||||
github.com/metacubex/utls v1.6.6 // 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/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
@@ -103,11 +101,12 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||||
golang.org/x/mod v0.20.0 // indirect
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
golang.org/x/net v0.28.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.24.0 // indirect
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
golang.org/x/tools v0.24.0 // indirect
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect`
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
19
core/go.sum
19
core/go.sum
@@ -1,7 +1,5 @@
|
|||||||
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
|
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
|
||||||
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
|
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/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 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
|
||||||
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
|
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
|
||||||
@@ -106,10 +104,12 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO
|
|||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
github.com/metacubex/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 h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
||||||
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58 h1:T6OxROLZBr9SOQxN5TzUslv81hEREy/dEgaUKVjaG7U=
|
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
|
||||||
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
|
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
|
||||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
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 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
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 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||||
@@ -162,9 +162,6 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
|||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
github.com/sagernet/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 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
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 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
|
||||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
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 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||||
@@ -190,9 +187,15 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
|||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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.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.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.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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
@@ -250,7 +253,7 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||||
|
|||||||
18
core/hub.go
18
core/hub.go
@@ -7,6 +7,7 @@ import "C"
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
bridge "core/dart-bridge"
|
bridge "core/dart-bridge"
|
||||||
|
"core/state"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
@@ -30,8 +31,6 @@ import (
|
|||||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentRawConfig = config.DefaultRawConfig()
|
|
||||||
|
|
||||||
var configParams = ConfigExtendedParams{}
|
var configParams = ConfigExtendedParams{}
|
||||||
|
|
||||||
var externalProviders = map[string]cp.Provider{}
|
var externalProviders = map[string]cp.Provider{}
|
||||||
@@ -124,7 +123,7 @@ func updateConfig(s *C.char, port C.longlong) {
|
|||||||
}
|
}
|
||||||
configParams = params.Params
|
configParams = params.Params
|
||||||
prof := decorationConfig(params.ProfileId, params.Config)
|
prof := decorationConfig(params.ProfileId, params.Config)
|
||||||
currentRawConfig = prof
|
state.CurrentRawConfig = prof
|
||||||
err = applyConfig()
|
err = applyConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.SendToPort(i, err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
@@ -184,7 +183,7 @@ func changeProxy(s *C.char) {
|
|||||||
|
|
||||||
//export getTraffic
|
//export getTraffic
|
||||||
func getTraffic() *C.char {
|
func getTraffic() *C.char {
|
||||||
up, down := statistic.DefaultManager.Current(state.OnlyProxy)
|
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
|
||||||
traffic := map[string]int64{
|
traffic := map[string]int64{
|
||||||
"up": up,
|
"up": up,
|
||||||
"down": down,
|
"down": down,
|
||||||
@@ -199,7 +198,7 @@ func getTraffic() *C.char {
|
|||||||
|
|
||||||
//export getTotalTraffic
|
//export getTotalTraffic
|
||||||
func getTotalTraffic() *C.char {
|
func getTotalTraffic() *C.char {
|
||||||
up, down := statistic.DefaultManager.Total(state.OnlyProxy)
|
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
|
||||||
traffic := map[string]int64{
|
traffic := map[string]int64{
|
||||||
"up": up,
|
"up": up,
|
||||||
"down": down,
|
"down": down,
|
||||||
@@ -378,27 +377,28 @@ func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
|
|||||||
geoTypeString := C.GoString(geoType)
|
geoTypeString := C.GoString(geoType)
|
||||||
geoNameString := C.GoString(geoName)
|
geoNameString := C.GoString(geoName)
|
||||||
go func() {
|
go func() {
|
||||||
|
path := constant.Path.Resolve(geoNameString)
|
||||||
switch geoTypeString {
|
switch geoTypeString {
|
||||||
case "MMDB":
|
case "MMDB":
|
||||||
err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
|
err := updater.UpdateMMDBWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.SendToPort(i, err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "ASN":
|
case "ASN":
|
||||||
err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
|
err := updater.UpdateASNWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.SendToPort(i, err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "GeoIp":
|
case "GeoIp":
|
||||||
err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
|
err := updater.UpdateGeoIpWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.SendToPort(i, err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "GeoSite":
|
case "GeoSite":
|
||||||
err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
|
err := updater.UpdateGeoSiteWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.SendToPort(i, err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,36 +2,33 @@ package main
|
|||||||
|
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
|
"core/state"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccessControl struct {
|
//export getCurrentProfileName
|
||||||
Mode string `json:"mode"`
|
func getCurrentProfileName() *C.char {
|
||||||
AcceptList []string `json:"acceptList"`
|
if state.CurrentState == nil {
|
||||||
RejectList []string `json:"rejectList"`
|
return C.CString("")
|
||||||
IsFilterSystemApp bool `json:"isFilterSystemApp"`
|
}
|
||||||
|
return C.CString(state.CurrentState.CurrentProfileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AndroidProps struct {
|
//export getAndroidVpnOptions
|
||||||
Enable bool `json:"enable"`
|
func getAndroidVpnOptions() *C.char {
|
||||||
AccessControl *AccessControl `json:"accessControl"`
|
options := state.AndroidVpnOptions{
|
||||||
AllowBypass bool `json:"allowBypass"`
|
Enable: state.CurrentState.Enable,
|
||||||
SystemProxy bool `json:"systemProxy"`
|
Port: state.CurrentRawConfig.MixedPort,
|
||||||
}
|
Ipv4Address: state.DefaultIpv4Address,
|
||||||
|
Ipv6Address: state.GetIpv6Address(),
|
||||||
type State struct {
|
AccessControl: state.CurrentState.AccessControl,
|
||||||
AndroidProps
|
SystemProxy: state.CurrentState.SystemProxy,
|
||||||
CurrentProfileName string `json:"currentProfileName"`
|
AllowBypass: state.CurrentState.AllowBypass,
|
||||||
MixedPort int `json:"mixedPort"`
|
BypassDomain: state.CurrentState.BypassDomain,
|
||||||
OnlyProxy bool `json:"onlyProxy"`
|
DnsServerAddress: state.GetDnsServerAddress(),
|
||||||
}
|
}
|
||||||
|
data, err := json.Marshal(options)
|
||||||
var state State
|
|
||||||
|
|
||||||
//export getState
|
|
||||||
func getState() *C.char {
|
|
||||||
data, err := json.Marshal(state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error:", err)
|
fmt.Println("Error:", err)
|
||||||
return C.CString("")
|
return C.CString("")
|
||||||
@@ -42,7 +39,7 @@ func getState() *C.char {
|
|||||||
//export setState
|
//export setState
|
||||||
func setState(s *C.char) {
|
func setState(s *C.char) {
|
||||||
paramsString := C.GoString(s)
|
paramsString := C.GoString(s)
|
||||||
err := json.Unmarshal([]byte(paramsString), &state)
|
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
59
core/state/state.go
Normal file
59
core/state/state.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package state
|
||||||
|
|
||||||
|
import "github.com/metacubex/mihomo/config"
|
||||||
|
|
||||||
|
var DefaultIpv4Address = "172.19.0.1/30"
|
||||||
|
var DefaultDnsAddress = "172.19.0.2"
|
||||||
|
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
||||||
|
|
||||||
|
var CurrentRawConfig = config.DefaultRawConfig()
|
||||||
|
|
||||||
|
type AndroidVpnOptions struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
AccessControl *AccessControl `json:"accessControl"`
|
||||||
|
AllowBypass bool `json:"allowBypass"`
|
||||||
|
SystemProxy bool `json:"systemProxy"`
|
||||||
|
BypassDomain []string `json:"bypassDomain"`
|
||||||
|
Ipv4Address string `json:"ipv4Address"`
|
||||||
|
Ipv6Address string `json:"ipv6Address"`
|
||||||
|
DnsServerAddress string `json:"dnsServerAddress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessControl struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
AcceptList []string `json:"acceptList"`
|
||||||
|
RejectList []string `json:"rejectList"`
|
||||||
|
IsFilterSystemApp bool `json:"isFilterSystemApp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AndroidVpnRawOptions struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
AccessControl *AccessControl `json:"accessControl"`
|
||||||
|
AllowBypass bool `json:"allowBypass"`
|
||||||
|
SystemProxy bool `json:"systemProxy"`
|
||||||
|
Ipv6 bool `json:"ipv6"`
|
||||||
|
BypassDomain []string `json:"bypassDomain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
AndroidVpnRawOptions
|
||||||
|
CurrentProfileName string `json:"currentProfileName"`
|
||||||
|
OnlyProxy bool `json:"onlyProxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var CurrentState = &State{}
|
||||||
|
|
||||||
|
func GetIpv6Address() string {
|
||||||
|
if CurrentState.Ipv6 {
|
||||||
|
return DefaultIpv6Address
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDnsServerAddress() string {
|
||||||
|
//prefix, _ := netip.ParsePrefix(DefaultIpv4Address)
|
||||||
|
//return prefix.Addr().String()
|
||||||
|
return DefaultDnsAddress
|
||||||
|
}
|
||||||
44
core/tun.go
44
core/tun.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"core/platform"
|
"core/platform"
|
||||||
t "core/tun"
|
t "core/tun"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -15,11 +16,9 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
"golang.org/x/sync/semaphore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var tunLock sync.Mutex
|
var tunLock sync.Mutex
|
||||||
var tun *t.Tun
|
|
||||||
var runTime *time.Time
|
var runTime *time.Time
|
||||||
|
|
||||||
type FdMap struct {
|
type FdMap struct {
|
||||||
@@ -35,7 +34,11 @@ func (cm *FdMap) Load(key int64) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
var fdMap FdMap
|
var (
|
||||||
|
tunListener *sing_tun.Listener
|
||||||
|
fdMap FdMap
|
||||||
|
fdCounter int64 = 0
|
||||||
|
)
|
||||||
|
|
||||||
//export startTUN
|
//export startTUN
|
||||||
func startTUN(fd C.int, port C.longlong) {
|
func startTUN(fd C.int, port C.longlong) {
|
||||||
@@ -56,30 +59,12 @@ func startTUN(fd C.int, port C.longlong) {
|
|||||||
go func() {
|
go func() {
|
||||||
tunLock.Lock()
|
tunLock.Lock()
|
||||||
defer tunLock.Unlock()
|
defer tunLock.Unlock()
|
||||||
|
|
||||||
if tun != nil {
|
|
||||||
tun.Close()
|
|
||||||
tun = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f := int(fd)
|
f := int(fd)
|
||||||
gateway := "172.16.0.1/30"
|
tunListener, _ = t.Start(f)
|
||||||
portal := "172.16.0.2"
|
if tunListener != nil {
|
||||||
dns := "0.0.0.0"
|
log.Infoln("TUN address: %v", tunListener.Address())
|
||||||
|
|
||||||
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()
|
now := time.Now()
|
||||||
|
|
||||||
runTime = &now
|
runTime = &now
|
||||||
@@ -108,9 +93,8 @@ func stopTun() {
|
|||||||
|
|
||||||
runTime = nil
|
runTime = nil
|
||||||
|
|
||||||
if tun != nil {
|
if tunListener != nil {
|
||||||
tun.Close()
|
_ = tunListener.Close()
|
||||||
tun = nil
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -137,18 +121,12 @@ func markSocket(fd Fd) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var fdCounter int64 = 0
|
|
||||||
|
|
||||||
func initSocketHook() {
|
func initSocketHook() {
|
||||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||||
if platform.ShouldBlockConnection() {
|
if platform.ShouldBlockConnection() {
|
||||||
return errBlocked
|
return errBlocked
|
||||||
}
|
}
|
||||||
return conn.Control(func(fd uintptr) {
|
return conn.Control(func(fd uintptr) {
|
||||||
if tun == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fdInt := int64(fd)
|
fdInt := int64(fd)
|
||||||
timeout := time.After(100 * time.Millisecond)
|
timeout := time.After(100 * time.Millisecond)
|
||||||
id := atomic.AddInt64(&fdCounter, 1)
|
id := atomic.AddInt64(&fdCounter, 1)
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package tun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/metacubex/mihomo/dns"
|
|
||||||
D "github.com/miekg/dns"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
|
|
||||||
if targetPort != 53 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return net.IPv4zero.Equal(dns) || target.Equal(dns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func relayDns(payload []byte) ([]byte, error) {
|
|
||||||
msg := &D.Msg{}
|
|
||||||
if err := msg.Unpack(payload); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := dns.ServeDNSWithDefaultServer(msg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SetRcode(msg, r.Rcode)
|
|
||||||
|
|
||||||
return r.Pack()
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package tun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/metacubex/mihomo/constant"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createMetadata(lAddr, rAddr *net.TCPAddr) *constant.Metadata {
|
|
||||||
return &constant.Metadata{
|
|
||||||
NetWork: constant.TCP,
|
|
||||||
Type: constant.SOCKS5,
|
|
||||||
SrcIP: lAddr.AddrPort().Addr(),
|
|
||||||
DstIP: rAddr.AddrPort().Addr(),
|
|
||||||
SrcPort: uint16(lAddr.Port),
|
|
||||||
DstPort: uint16(rAddr.Port),
|
|
||||||
Host: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
211
core/tun/tun.go
211
core/tun/tun.go
@@ -4,182 +4,65 @@ package tun
|
|||||||
|
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
"core/state"
|
||||||
"encoding/binary"
|
LC "github.com/metacubex/mihomo/listener/config"
|
||||||
"github.com/Kr328/tun2socket"
|
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||||
"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/log"
|
||||||
"github.com/metacubex/mihomo/transport/socks5"
|
|
||||||
"github.com/metacubex/mihomo/tunnel"
|
"github.com/metacubex/mihomo/tunnel"
|
||||||
"golang.org/x/sync/semaphore"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"net/netip"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tun struct {
|
type Props struct {
|
||||||
Closer io.Closer
|
Fd int `json:"fd"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
Closed bool
|
Gateway6 string `json:"gateway6"`
|
||||||
Limit *semaphore.Weighted
|
Portal string `json:"portal"`
|
||||||
|
Portal6 string `json:"portal6"`
|
||||||
|
Dns string `json:"dns"`
|
||||||
|
Dns6 string `json:"dns6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tun) Close() {
|
func Start(fd int) (*sing_tun.Listener, error) {
|
||||||
_ = t.Limit.Acquire(context.TODO(), 4)
|
var prefix4 []netip.Prefix
|
||||||
defer t.Limit.Release(4)
|
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
panic(err.Error())
|
log.Errorln("startTUN error:", err)
|
||||||
} else {
|
return nil, err
|
||||||
network.IP = ip
|
|
||||||
}
|
}
|
||||||
stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal))
|
prefix4 = append(prefix4, tempPrefix4)
|
||||||
|
var prefix6 []netip.Prefix
|
||||||
|
if state.CurrentState.Ipv6 {
|
||||||
|
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("startTUN error:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prefix6 = append(prefix6, tempPrefix6)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dnsHijack []string
|
||||||
|
dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53"))
|
||||||
|
|
||||||
|
options := LC.Tun{
|
||||||
|
Enable: true,
|
||||||
|
Device: state.CurrentRawConfig.Tun.Device,
|
||||||
|
Stack: state.CurrentRawConfig.Tun.Stack,
|
||||||
|
DNSHijack: dnsHijack,
|
||||||
|
AutoRoute: false,
|
||||||
|
AutoDetectInterface: false,
|
||||||
|
Inet4Address: prefix4,
|
||||||
|
Inet6Address: prefix6,
|
||||||
|
MTU: 9000,
|
||||||
|
FileDescriptor: fd,
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := sing_tun.New(options, tunnel.Tunnel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = device.Close()
|
log.Errorln("startTUN error:", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsAddr := net.ParseIP(dns)
|
return listener, nil
|
||||||
|
|
||||||
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 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lAddr := conn.LocalAddr().(*net.TCPAddr)
|
|
||||||
rAddr := conn.RemoteAddr().(*net.TCPAddr)
|
|
||||||
|
|
||||||
if ipv4LoopBack.Contains(rAddr.IP) {
|
|
||||||
_ = conn.Close()
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
|
|
||||||
go func() {
|
|
||||||
defer func(conn net.Conn) {
|
|
||||||
_ = conn.Close()
|
|
||||||
}(conn)
|
|
||||||
|
|
||||||
buf := pool.Get(pool.UDPBufferSize)
|
|
||||||
defer func(buf []byte) {
|
|
||||||
_ = pool.Put(buf)
|
|
||||||
}(buf)
|
|
||||||
|
|
||||||
for {
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(constant.DefaultTCPTimeout))
|
|
||||||
|
|
||||||
length := uint16(0)
|
|
||||||
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if int(length) > len(buf) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := conn.Read(buf[:length])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := relayDns(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = conn.Write(msg)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
go tunnel.Tunnel.HandleTCPConn(conn, createMetadata(lAddr, rAddr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
udp := func() {
|
|
||||||
defer func(udp *nat.UDP) {
|
|
||||||
_ = udp.Close()
|
|
||||||
}(stack.UDP())
|
|
||||||
defer log.Debugln("UDP: closed")
|
|
||||||
|
|
||||||
for {
|
|
||||||
buf := pool.Get(pool.UDPBufferSize)
|
|
||||||
|
|
||||||
n, lRAddr, rRAddr, err := stack.UDP().ReadFrom(buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := buf[:n]
|
|
||||||
lAddr := lRAddr.(*net.UDPAddr)
|
|
||||||
rAddr := rRAddr.(*net.UDPAddr)
|
|
||||||
|
|
||||||
if ipv4LoopBack.Contains(rAddr.IP) {
|
|
||||||
_ = pool.Put(buf)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
|
|
||||||
go func() {
|
|
||||||
defer func(buf []byte) {
|
|
||||||
_ = pool.Put(buf)
|
|
||||||
}(buf)
|
|
||||||
|
|
||||||
msg, err := relayDns(raw)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = stack.UDP().WriteTo(msg, rAddr, lAddr)
|
|
||||||
}()
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt := &packet{
|
|
||||||
local: lAddr,
|
|
||||||
data: raw,
|
|
||||||
writeBack: func(b []byte, addr net.Addr) (int, error) {
|
|
||||||
return stack.UDP().WriteTo(b, addr, lAddr)
|
|
||||||
},
|
|
||||||
drop: func() {
|
|
||||||
_ = pool.Put(buf)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnel.Tunnel.HandleUDPPacket(inbound.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, constant.SOCKS5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go tcp()
|
|
||||||
go udp()
|
|
||||||
|
|
||||||
return stack, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package tun
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
type packet struct {
|
|
||||||
local *net.UDPAddr
|
|
||||||
data []byte
|
|
||||||
writeBack func(b []byte, addr net.Addr) (int, error)
|
|
||||||
drop func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pkt *packet) Data() []byte {
|
|
||||||
return pkt.data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pkt *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) {
|
|
||||||
return pkt.writeBack(b, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pkt *packet) Drop() {
|
|
||||||
pkt.drop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pkt *packet) LocalAddr() net.Addr {
|
|
||||||
return pkt.local
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:fl_clash/l10n/l10n.dart';
|
import 'package:fl_clash/l10n/l10n.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||||
|
import 'package:fl_clash/manager/manager.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/proxy_container.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -28,11 +27,13 @@ runAppWithPreferences(
|
|||||||
ChangeNotifierProvider<Config>(
|
ChangeNotifierProvider<Config>(
|
||||||
create: (_) => config,
|
create: (_) => config,
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProvider<AppFlowingState>(
|
||||||
|
create: (_) => AppFlowingState(),
|
||||||
|
),
|
||||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
||||||
create: (_) => appState,
|
create: (_) => appState,
|
||||||
update: (_, config, clashConfig, appState) {
|
update: (_, config, clashConfig, appState) {
|
||||||
appState?.mode = clashConfig.mode;
|
appState?.mode = clashConfig.mode;
|
||||||
appState?.isCompatible = config.isCompatible;
|
|
||||||
appState?.selectedMap = config.currentSelectedMap;
|
appState?.selectedMap = config.currentSelectedMap;
|
||||||
return appState!;
|
return appState!;
|
||||||
},
|
},
|
||||||
@@ -53,6 +54,7 @@ class Application extends StatefulWidget {
|
|||||||
|
|
||||||
class ApplicationState extends State<Application> {
|
class ApplicationState extends State<Application> {
|
||||||
late SystemColorSchemes systemColorSchemes;
|
late SystemColorSchemes systemColorSchemes;
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||||
@@ -81,7 +83,9 @@ class ApplicationState extends State<Application> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_initTimer();
|
||||||
globalState.appController = AppController(context);
|
globalState.appController = AppController(context);
|
||||||
|
globalState.measure = Measure.of(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
final currentContext = globalState.navigatorKey.currentContext;
|
final currentContext = globalState.navigatorKey.currentContext;
|
||||||
if (currentContext != null) {
|
if (currentContext != null) {
|
||||||
@@ -92,18 +96,36 @@ class ApplicationState extends State<Application> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initTimer() {
|
||||||
|
_cancelTimer();
|
||||||
|
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
globalState.appController.updateGroupDebounce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancelTimer() {
|
||||||
|
if (timer != null) {
|
||||||
|
timer?.cancel();
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buildApp(Widget app) {
|
_buildApp(Widget app) {
|
||||||
if (system.isDesktop) {
|
if (system.isDesktop) {
|
||||||
return WindowContainer(
|
return WindowManager(
|
||||||
child: TrayContainer(
|
child: TrayManager(
|
||||||
child: ProxyContainer(
|
child: HotKeyManager(
|
||||||
child: app,
|
child: ProxyManager(
|
||||||
|
child: app,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return AndroidContainer(
|
return AndroidManager(
|
||||||
child: TileContainer(
|
child: TileManager(
|
||||||
child: app,
|
child: app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -115,7 +137,7 @@ class ApplicationState extends State<Application> {
|
|||||||
child: page,
|
child: page,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return VpnContainer(
|
return VpnManager(
|
||||||
child: page,
|
child: page,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -136,11 +158,11 @@ class ApplicationState extends State<Application> {
|
|||||||
@override
|
@override
|
||||||
Widget build(context) {
|
Widget build(context) {
|
||||||
return _buildApp(
|
return _buildApp(
|
||||||
AppStateContainer(
|
AppStateManager(
|
||||||
child: ClashContainer(
|
child: ClashManager(
|
||||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||||
selector: (_, appState, config) => ApplicationSelectorState(
|
selector: (_, appState, config) => ApplicationSelectorState(
|
||||||
locale: config.locale,
|
locale: config.appSetting.locale,
|
||||||
themeMode: config.themeMode,
|
themeMode: config.themeMode,
|
||||||
primaryColor: config.primaryColor,
|
primaryColor: config.primaryColor,
|
||||||
prueBlack: config.prueBlack,
|
prueBlack: config.prueBlack,
|
||||||
@@ -158,8 +180,15 @@ class ApplicationState extends State<Application> {
|
|||||||
GlobalWidgetsLocalizations.delegate
|
GlobalWidgetsLocalizations.delegate
|
||||||
],
|
],
|
||||||
builder: (_, child) {
|
builder: (_, child) {
|
||||||
return MediaContainer(
|
return LayoutBuilder(
|
||||||
child: _buildPage(child!),
|
builder: (_, container) {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
final maxWidth = container.maxWidth;
|
||||||
|
if (appController.appState.viewWidth != maxWidth) {
|
||||||
|
globalState.appController.updateViewWidth(maxWidth);
|
||||||
|
}
|
||||||
|
return _buildPage(child!);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
scrollBehavior: BaseScrollBehavior(),
|
scrollBehavior: BaseScrollBehavior(),
|
||||||
@@ -203,5 +232,6 @@ class ApplicationState extends State<Application> {
|
|||||||
linkManager.destroy();
|
linkManager.destroy();
|
||||||
await globalState.appController.savePreferences();
|
await globalState.appController.savePreferences();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
_cancelTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class ClashCore {
|
|||||||
final externalProviderRawString =
|
final externalProviderRawString =
|
||||||
externalProviderRaw.cast<Utf8>().toDartString();
|
externalProviderRaw.cast<Utf8>().toDartString();
|
||||||
clashFFI.freeCString(externalProviderRaw);
|
clashFFI.freeCString(externalProviderRaw);
|
||||||
if(externalProviderRawString.isEmpty) return null;
|
if (externalProviderRawString.isEmpty) return null;
|
||||||
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
|
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,13 +287,18 @@ class ClashCore {
|
|||||||
malloc.free(stateChar);
|
malloc.free(stateChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreState getState() {
|
String getCurrentProfileName() {
|
||||||
final stateRaw = clashFFI.getState();
|
final currentProfileRaw = clashFFI.getCurrentProfileName();
|
||||||
final state = json.decode(
|
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
|
||||||
stateRaw.cast<Utf8>().toDartString(),
|
clashFFI.freeCString(currentProfileRaw);
|
||||||
);
|
return currentProfile;
|
||||||
clashFFI.freeCString(stateRaw);
|
}
|
||||||
return CoreState.fromJson(state);
|
|
||||||
|
AndroidVpnOptions getAndroidVpnOptions() {
|
||||||
|
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
|
||||||
|
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
|
||||||
|
clashFFI.freeCString(vpnOptionsRaw);
|
||||||
|
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
Traffic getTraffic() {
|
Traffic getTraffic() {
|
||||||
@@ -327,6 +332,13 @@ class ClashCore {
|
|||||||
clashFFI.startTUN(fd, port);
|
clashFFI.startTUN(fd, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateDns(String dns) {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.updateDns(dnsChar);
|
||||||
|
malloc.free(dnsChar);
|
||||||
|
}
|
||||||
|
|
||||||
requestGc() {
|
requestGc() {
|
||||||
clashFFI.forceGc();
|
clashFFI.forceGc();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5144,6 +5144,20 @@ class ClashFFI {
|
|||||||
late final __FCmulcr =
|
late final __FCmulcr =
|
||||||
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
|
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
|
||||||
|
|
||||||
|
void updateDns(
|
||||||
|
ffi.Pointer<ffi.Char> s,
|
||||||
|
) {
|
||||||
|
return _updateDns(
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _updateDnsPtr =
|
||||||
|
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
|
||||||
|
'updateDns');
|
||||||
|
late final _updateDns =
|
||||||
|
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
return _start();
|
return _start();
|
||||||
}
|
}
|
||||||
@@ -5543,14 +5557,25 @@ class ClashFFI {
|
|||||||
late final _setProcessMap =
|
late final _setProcessMap =
|
||||||
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
ffi.Pointer<ffi.Char> getState() {
|
ffi.Pointer<ffi.Char> getCurrentProfileName() {
|
||||||
return _getState();
|
return _getCurrentProfileName();
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _getStatePtr =
|
late final _getCurrentProfileNamePtr =
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('getState');
|
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||||
late final _getState =
|
'getCurrentProfileName');
|
||||||
_getStatePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
late final _getCurrentProfileName =
|
||||||
|
_getCurrentProfileNamePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||||
|
|
||||||
|
ffi.Pointer<ffi.Char> getAndroidVpnOptions() {
|
||||||
|
return _getAndroidVpnOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _getAndroidVpnOptionsPtr =
|
||||||
|
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
|
||||||
|
'getAndroidVpnOptions');
|
||||||
|
late final _getAndroidVpnOptions =
|
||||||
|
_getAndroidVpnOptionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||||
|
|
||||||
void setState(
|
void setState(
|
||||||
ffi.Pointer<ffi.Char> s,
|
ffi.Pointer<ffi.Char> s,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
|
||||||
class Android {
|
class Android {
|
||||||
init() async {
|
init() async {
|
||||||
app?.onExit = () {};
|
app?.onExit = () async {
|
||||||
|
await globalState.appController.savePreferences();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,6 @@ export 'windows.dart';
|
|||||||
export 'iterable.dart';
|
export 'iterable.dart';
|
||||||
export 'scroll.dart';
|
export 'scroll.dart';
|
||||||
export 'icons.dart';
|
export 'icons.dart';
|
||||||
export 'http.dart';
|
export 'http.dart';
|
||||||
|
export 'keyboard.dart';
|
||||||
|
export 'network.dart';
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/clash_config.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'system.dart';
|
import 'system.dart';
|
||||||
|
|
||||||
@@ -16,14 +18,18 @@ const mmdbFileName = "geoip.metadb";
|
|||||||
const asnFileName = "ASN.mmdb";
|
const asnFileName = "ASN.mmdb";
|
||||||
const geoIpFileName = "GeoIP.dat";
|
const geoIpFileName = "GeoIP.dat";
|
||||||
const geoSiteFileName = "GeoSite.dat";
|
const geoSiteFileName = "GeoSite.dat";
|
||||||
final double kHeaderHeight = system.isDesktop ? 40 : 0;
|
final double kHeaderHeight = system.isDesktop
|
||||||
|
? !Platform.isMacOS
|
||||||
|
? 40
|
||||||
|
: 26
|
||||||
|
: 0;
|
||||||
const GeoXMap defaultGeoXMap = {
|
const GeoXMap defaultGeoXMap = {
|
||||||
"mmdb":
|
"mmdb":
|
||||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||||
"asn":
|
"asn":
|
||||||
"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
|
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||||
"geoip":
|
"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":
|
"geosite":
|
||||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
||||||
};
|
};
|
||||||
@@ -46,6 +52,21 @@ final filter = ImageFilter.blur(
|
|||||||
tileMode: TileMode.mirror,
|
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 = {
|
const viewModeColumnsMap = {
|
||||||
ViewMode.mobile: [2, 1],
|
ViewMode.mobile: [2, 1],
|
||||||
ViewMode.laptop: [3, 2],
|
ViewMode.laptop: [3, 2],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension BuildContextExtension on BuildContext {
|
|||||||
return MediaQuery.of(this).size;
|
return MediaQuery.of(this).size;
|
||||||
}
|
}
|
||||||
|
|
||||||
double get width {
|
double get viewWidth {
|
||||||
return appSize.width;
|
return appSize.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ class FlClashHttpOverrides extends HttpOverrides {
|
|||||||
client.badCertificateCallback = (_, __, ___) => true;
|
client.badCertificateCallback = (_, __, ___) => true;
|
||||||
client.findProxy = (url) {
|
client.findProxy = (url) {
|
||||||
debugPrint("find $url");
|
debugPrint("find $url");
|
||||||
final port = globalState.appController.clashConfig.mixedPort;
|
final appController = globalState.appController;
|
||||||
final isStart = globalState.appController.appState.isStart;
|
final port = appController.clashConfig.mixedPort;
|
||||||
|
final isStart = appController.appFlowingState.isStart;
|
||||||
if (!isStart) return "DIRECT";
|
if (!isStart) return "DIRECT";
|
||||||
return "PROXY localhost:$port";
|
return "PROXY localhost:$port";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1; // 这行理论上不会执行到,但为了完整性保留
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
106
lib/common/keyboard.dart
Normal file
106
lib/common/keyboard.dart
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:uni_platform/uni_platform.dart';
|
||||||
|
|
||||||
|
final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
|
||||||
|
<PhysicalKeyboardKey, String>{
|
||||||
|
PhysicalKeyboardKey.keyA: 'A',
|
||||||
|
PhysicalKeyboardKey.keyB: 'B',
|
||||||
|
PhysicalKeyboardKey.keyC: 'C',
|
||||||
|
PhysicalKeyboardKey.keyD: 'D',
|
||||||
|
PhysicalKeyboardKey.keyE: 'E',
|
||||||
|
PhysicalKeyboardKey.keyF: 'F',
|
||||||
|
PhysicalKeyboardKey.keyG: 'G',
|
||||||
|
PhysicalKeyboardKey.keyH: 'H',
|
||||||
|
PhysicalKeyboardKey.keyI: 'I',
|
||||||
|
PhysicalKeyboardKey.keyJ: 'J',
|
||||||
|
PhysicalKeyboardKey.keyK: 'K',
|
||||||
|
PhysicalKeyboardKey.keyL: 'L',
|
||||||
|
PhysicalKeyboardKey.keyM: 'M',
|
||||||
|
PhysicalKeyboardKey.keyN: 'N',
|
||||||
|
PhysicalKeyboardKey.keyO: 'O',
|
||||||
|
PhysicalKeyboardKey.keyP: 'P',
|
||||||
|
PhysicalKeyboardKey.keyQ: 'Q',
|
||||||
|
PhysicalKeyboardKey.keyR: 'R',
|
||||||
|
PhysicalKeyboardKey.keyS: 'S',
|
||||||
|
PhysicalKeyboardKey.keyT: 'T',
|
||||||
|
PhysicalKeyboardKey.keyU: 'U',
|
||||||
|
PhysicalKeyboardKey.keyV: 'V',
|
||||||
|
PhysicalKeyboardKey.keyW: 'W',
|
||||||
|
PhysicalKeyboardKey.keyX: 'X',
|
||||||
|
PhysicalKeyboardKey.keyY: 'Y',
|
||||||
|
PhysicalKeyboardKey.keyZ: 'Z',
|
||||||
|
PhysicalKeyboardKey.digit1: '1',
|
||||||
|
PhysicalKeyboardKey.digit2: '2',
|
||||||
|
PhysicalKeyboardKey.digit3: '3',
|
||||||
|
PhysicalKeyboardKey.digit4: '4',
|
||||||
|
PhysicalKeyboardKey.digit5: '5',
|
||||||
|
PhysicalKeyboardKey.digit6: '6',
|
||||||
|
PhysicalKeyboardKey.digit7: '7',
|
||||||
|
PhysicalKeyboardKey.digit8: '8',
|
||||||
|
PhysicalKeyboardKey.digit9: '9',
|
||||||
|
PhysicalKeyboardKey.digit0: '0',
|
||||||
|
PhysicalKeyboardKey.enter: 'ENTER',
|
||||||
|
PhysicalKeyboardKey.escape: 'ESCAPE',
|
||||||
|
PhysicalKeyboardKey.backspace: 'BACKSPACE',
|
||||||
|
PhysicalKeyboardKey.tab: 'TAB',
|
||||||
|
PhysicalKeyboardKey.space: 'SPACE',
|
||||||
|
PhysicalKeyboardKey.minus: '-',
|
||||||
|
PhysicalKeyboardKey.equal: '=',
|
||||||
|
PhysicalKeyboardKey.bracketLeft: '[',
|
||||||
|
PhysicalKeyboardKey.bracketRight: ']',
|
||||||
|
PhysicalKeyboardKey.backslash: '\\',
|
||||||
|
PhysicalKeyboardKey.semicolon: ';',
|
||||||
|
PhysicalKeyboardKey.quote: '"',
|
||||||
|
PhysicalKeyboardKey.backquote: '`',
|
||||||
|
PhysicalKeyboardKey.comma: ',',
|
||||||
|
PhysicalKeyboardKey.period: '.',
|
||||||
|
PhysicalKeyboardKey.slash: '/',
|
||||||
|
PhysicalKeyboardKey.capsLock: 'CAPSLOCK',
|
||||||
|
PhysicalKeyboardKey.f1: 'F1',
|
||||||
|
PhysicalKeyboardKey.f2: 'F2',
|
||||||
|
PhysicalKeyboardKey.f3: 'F3',
|
||||||
|
PhysicalKeyboardKey.f4: 'F4',
|
||||||
|
PhysicalKeyboardKey.f5: 'F5',
|
||||||
|
PhysicalKeyboardKey.f6: 'F6',
|
||||||
|
PhysicalKeyboardKey.f7: 'F7',
|
||||||
|
PhysicalKeyboardKey.f8: 'F8',
|
||||||
|
PhysicalKeyboardKey.f9: 'F9',
|
||||||
|
PhysicalKeyboardKey.f10: 'F10',
|
||||||
|
PhysicalKeyboardKey.f11: 'F11',
|
||||||
|
PhysicalKeyboardKey.f12: 'F12',
|
||||||
|
PhysicalKeyboardKey.home: 'HOME',
|
||||||
|
PhysicalKeyboardKey.pageUp: 'PAGEUP',
|
||||||
|
PhysicalKeyboardKey.delete: 'DELETE',
|
||||||
|
PhysicalKeyboardKey.end: 'END',
|
||||||
|
PhysicalKeyboardKey.pageDown: 'PAGEDOWN',
|
||||||
|
PhysicalKeyboardKey.arrowRight: '→',
|
||||||
|
PhysicalKeyboardKey.arrowLeft: '←',
|
||||||
|
PhysicalKeyboardKey.arrowDown: '↓',
|
||||||
|
PhysicalKeyboardKey.arrowUp: '↑',
|
||||||
|
PhysicalKeyboardKey.controlLeft: "CTRL",
|
||||||
|
PhysicalKeyboardKey.shiftLeft: 'SHIFT',
|
||||||
|
PhysicalKeyboardKey.altLeft: "ALT",
|
||||||
|
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '⌘' : 'WIN',
|
||||||
|
PhysicalKeyboardKey.controlRight: "CTRL",
|
||||||
|
PhysicalKeyboardKey.shiftRight: 'SHIFT',
|
||||||
|
PhysicalKeyboardKey.altRight: "ALT",
|
||||||
|
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '⌘' : 'WIN',
|
||||||
|
PhysicalKeyboardKey.fn: 'FN',
|
||||||
|
};
|
||||||
|
|
||||||
|
extension KeyboardKeyExt on KeyboardKey {
|
||||||
|
String get label {
|
||||||
|
PhysicalKeyboardKey? physicalKey;
|
||||||
|
if (this is LogicalKeyboardKey) {
|
||||||
|
physicalKey = (this as LogicalKeyboardKey).physicalKey;
|
||||||
|
} else if (this is PhysicalKeyboardKey) {
|
||||||
|
physicalKey = this as PhysicalKeyboardKey;
|
||||||
|
}
|
||||||
|
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -58,22 +58,7 @@ class AutoLaunch {
|
|||||||
|
|
||||||
Future<bool> windowsEnable() async {
|
Future<bool> windowsEnable() async {
|
||||||
await disable();
|
await disable();
|
||||||
return windows?.runas(
|
return await windows?.registerTask(appName) ?? false;
|
||||||
'schtasks',
|
|
||||||
[
|
|
||||||
'/Create',
|
|
||||||
'/SC',
|
|
||||||
'ONLOGON',
|
|
||||||
'/TN',
|
|
||||||
appName,
|
|
||||||
'/TR',
|
|
||||||
'"${Platform.resolvedExecutable}"',
|
|
||||||
'/RL',
|
|
||||||
'HIGHEST',
|
|
||||||
'/F'
|
|
||||||
].join(" "),
|
|
||||||
) ??
|
|
||||||
false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> disable() async {
|
Future<bool> disable() async {
|
||||||
@@ -81,9 +66,9 @@ class AutoLaunch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(AutoLaunchState state) async {
|
updateStatus(AutoLaunchState state) async {
|
||||||
final isOpenTun = state.isOpenTun;
|
final isAdminAutoLaunch = state.isAdminAutoLaunch;
|
||||||
final isAutoLaunch = state.isAutoLaunch;
|
final isAutoLaunch = state.isAutoLaunch;
|
||||||
if (Platform.isWindows && isOpenTun) {
|
if (Platform.isWindows && isAdminAutoLaunch) {
|
||||||
if (await windowsIsEnable == isAutoLaunch) return;
|
if (await windowsIsEnable == isAutoLaunch) return;
|
||||||
if (isAutoLaunch) {
|
if (isAutoLaunch) {
|
||||||
final isEnable = await windowsEnable();
|
final isEnable = await windowsEnable();
|
||||||
|
|||||||
@@ -15,4 +15,10 @@ extension ListExtension<T> on List<T> {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<T> safeSublist(int start) {
|
||||||
|
if(start <= 0) return this;
|
||||||
|
if(start > length) return [];
|
||||||
|
return sublist(start);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ class Measure {
|
|||||||
final TextScaler _textScale;
|
final TextScaler _textScale;
|
||||||
late BuildContext context;
|
late BuildContext context;
|
||||||
|
|
||||||
Measure.of(this.context) : _textScale = MediaQuery.of(context).textScaler;
|
Measure.of(this.context)
|
||||||
|
: _textScale = TextScaler.linear(
|
||||||
|
WidgetsBinding.instance.platformDispatcher.textScaleFactor);
|
||||||
|
|
||||||
Size computeTextSize(Text text) {
|
Size computeTextSize(Text text) {
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
@@ -19,6 +21,7 @@ class Measure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double? _bodyMediumHeight;
|
double? _bodyMediumHeight;
|
||||||
|
Size? _bodyLargeSize;
|
||||||
double? _bodySmallHeight;
|
double? _bodySmallHeight;
|
||||||
double? _labelSmallHeight;
|
double? _labelSmallHeight;
|
||||||
double? _labelMediumHeight;
|
double? _labelMediumHeight;
|
||||||
@@ -28,17 +31,28 @@ class Measure {
|
|||||||
double get bodyMediumHeight {
|
double get bodyMediumHeight {
|
||||||
_bodyMediumHeight ??= computeTextSize(
|
_bodyMediumHeight ??= computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
"",
|
"X",
|
||||||
style: context.textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
).height;
|
).height;
|
||||||
return _bodyMediumHeight!;
|
return _bodyMediumHeight!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Size get bodyLargeSize {
|
||||||
|
_bodyLargeSize ??= computeTextSize(
|
||||||
|
Text(
|
||||||
|
"X",
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _bodyLargeSize!;
|
||||||
|
}
|
||||||
|
|
||||||
double get bodySmallHeight {
|
double get bodySmallHeight {
|
||||||
_bodySmallHeight ??= computeTextSize(
|
_bodySmallHeight ??= computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
"",
|
"X",
|
||||||
style: context.textTheme.bodySmall,
|
style: context.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
).height;
|
).height;
|
||||||
@@ -48,7 +62,7 @@ class Measure {
|
|||||||
double get labelSmallHeight {
|
double get labelSmallHeight {
|
||||||
_labelSmallHeight ??= computeTextSize(
|
_labelSmallHeight ??= computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
"",
|
"X",
|
||||||
style: context.textTheme.labelSmall,
|
style: context.textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
).height;
|
).height;
|
||||||
@@ -58,7 +72,7 @@ class Measure {
|
|||||||
double get labelMediumHeight {
|
double get labelMediumHeight {
|
||||||
_labelMediumHeight ??= computeTextSize(
|
_labelMediumHeight ??= computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
"",
|
"X",
|
||||||
style: context.textTheme.labelMedium,
|
style: context.textTheme.labelMedium,
|
||||||
),
|
),
|
||||||
).height;
|
).height;
|
||||||
@@ -68,7 +82,7 @@ class Measure {
|
|||||||
double get titleLargeHeight {
|
double get titleLargeHeight {
|
||||||
_titleLargeHeight ??= computeTextSize(
|
_titleLargeHeight ??= computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
"",
|
"X",
|
||||||
style: context.textTheme.titleLarge,
|
style: context.textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
).height;
|
).height;
|
||||||
@@ -78,7 +92,7 @@ class Measure {
|
|||||||
double get titleMediumHeight {
|
double get titleMediumHeight {
|
||||||
_titleMediumHeight ??= computeTextSize(
|
_titleMediumHeight ??= computeTextSize(
|
||||||
Text(
|
Text(
|
||||||
"",
|
"X",
|
||||||
style: context.textTheme.titleMedium,
|
style: context.textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
).height;
|
).height;
|
||||||
|
|||||||
25
lib/common/network.dart
Normal file
25
lib/common/network.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
extension NetworkInterfaceExt on NetworkInterface {
|
||||||
|
bool get isWifi {
|
||||||
|
final nameLowCase = name.toLowerCase();
|
||||||
|
if (nameLowCase.contains('wlan') ||
|
||||||
|
nameLowCase.contains('wi-fi') ||
|
||||||
|
nameLowCase == 'en0' ||
|
||||||
|
nameLowCase == 'eth0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get includesIPv4 {
|
||||||
|
return addresses.any((addr) => addr.isIPv4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InternetAddressExt on InternetAddress {
|
||||||
|
bool get isIPv4 {
|
||||||
|
return type == InternetAddressType.IPv4;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,12 +100,18 @@ class Other {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getTrayIconPath() {
|
String getTrayIconPath({
|
||||||
if (Platform.isWindows) {
|
required bool isStart,
|
||||||
return "assets/images/icon.ico";
|
required Brightness brightness,
|
||||||
} else {
|
}) {
|
||||||
return "assets/images/icon_monochrome.png";
|
final suffix = Platform.isWindows ? "ico" : "png";
|
||||||
|
if (!isStart && Platform.isWindows) {
|
||||||
|
return switch (brightness) {
|
||||||
|
Brightness.dark => "assets/images/icon_white.$suffix",
|
||||||
|
Brightness.light => "assets/images/icon_black.$suffix",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return "assets/images/icon.$suffix";
|
||||||
}
|
}
|
||||||
|
|
||||||
int compareVersions(String version1, String version2) {
|
int compareVersions(String version1, String version2) {
|
||||||
@@ -165,14 +171,18 @@ class Other {
|
|||||||
if (disposition == null) return null;
|
if (disposition == null) return null;
|
||||||
final parseValue = HeaderValue.parse(disposition);
|
final parseValue = HeaderValue.parse(disposition);
|
||||||
final parameters = parseValue.parameters;
|
final parameters = parseValue.parameters;
|
||||||
final key = parameters.keys
|
final fileNamePointKey = parameters.keys
|
||||||
.firstWhere((key) => key.startsWith("filename"), orElse: () => '');
|
.firstWhere((key) => key == "filename*", orElse: () => "");
|
||||||
if (key.isEmpty) return null;
|
if (fileNamePointKey.isNotEmpty) {
|
||||||
if (key == "filename*") {
|
final res = parameters[fileNamePointKey]?.split("''") ?? [];
|
||||||
return Uri.decodeComponent((parameters[key] ?? "").split("'").last);
|
if (res.length >= 2) {
|
||||||
} else {
|
return Uri.decodeComponent(res[1]);
|
||||||
return parameters[key];
|
}
|
||||||
}
|
}
|
||||||
|
final fileNameKey = parameters.keys
|
||||||
|
.firstWhere((key) => key == "filename", orElse: () => "");
|
||||||
|
if (fileNameKey.isEmpty) return null;
|
||||||
|
return parameters[fileNameKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
double getViewWidth() {
|
double getViewWidth() {
|
||||||
@@ -201,9 +211,9 @@ class Other {
|
|||||||
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
|
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
|
||||||
final columns = max((viewWidth / 300).ceil(), 2);
|
final columns = max((viewWidth / 300).ceil(), 2);
|
||||||
return switch (proxiesLayout) {
|
return switch (proxiesLayout) {
|
||||||
ProxiesLayout.tight => columns - 1,
|
ProxiesLayout.tight => columns + 1,
|
||||||
ProxiesLayout.standard => columns,
|
ProxiesLayout.standard => columns,
|
||||||
ProxiesLayout.loose => columns + 1,
|
ProxiesLayout.loose => columns - 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +224,15 @@ class Other {
|
|||||||
String getBackupFileName() {
|
String getBackupFileName() {
|
||||||
return "${appName}_backup_${DateTime.now().show}.zip";
|
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();
|
final other = Other();
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import 'constant.dart';
|
|||||||
|
|
||||||
class AppPath {
|
class AppPath {
|
||||||
static AppPath? _instance;
|
static AppPath? _instance;
|
||||||
Completer<Directory> cacheDir = Completer();
|
Completer<Directory> dataDir = Completer();
|
||||||
Completer<Directory> downloadDir = Completer();
|
Completer<Directory> downloadDir = Completer();
|
||||||
|
Completer<Directory> tempDir = Completer();
|
||||||
|
late String appDirPath;
|
||||||
|
|
||||||
// Future<Directory> _createDesktopCacheDir() async {
|
// Future<Directory> _createDesktopCacheDir() async {
|
||||||
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
|
|
||||||
// final dir = Directory(path);
|
// final dir = Directory(path);
|
||||||
// if (await dir.exists()) {
|
// if (await dir.exists()) {
|
||||||
// await dir.create(recursive: true);
|
// await dir.create(recursive: true);
|
||||||
@@ -21,8 +22,12 @@ class AppPath {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
AppPath._internal() {
|
AppPath._internal() {
|
||||||
|
appDirPath = join(dirname(Platform.resolvedExecutable));
|
||||||
getApplicationSupportDirectory().then((value) {
|
getApplicationSupportDirectory().then((value) {
|
||||||
cacheDir.complete(value);
|
dataDir.complete(value);
|
||||||
|
});
|
||||||
|
getTemporaryDirectory().then((value){
|
||||||
|
tempDir.complete(value);
|
||||||
});
|
});
|
||||||
getDownloadsDirectory().then((value) {
|
getDownloadsDirectory().then((value) {
|
||||||
downloadDir.complete(value);
|
downloadDir.complete(value);
|
||||||
@@ -49,12 +54,12 @@ class AppPath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getHomeDirPath() async {
|
Future<String> getHomeDirPath() async {
|
||||||
final directory = await cacheDir.future;
|
final directory = await dataDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getProfilesPath() async {
|
Future<String> getProfilesPath() async {
|
||||||
final directory = await cacheDir.future;
|
final directory = await dataDir.future;
|
||||||
return join(directory.path, profilesDirectoryName);
|
return join(directory.path, profilesDirectoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +68,11 @@ class AppPath {
|
|||||||
final directory = await getProfilesPath();
|
final directory = await getProfilesPath();
|
||||||
return join(directory, "$id.yaml");
|
return join(directory, "$id.yaml");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> get tempPath async {
|
||||||
|
final directory = await tempDir.future;
|
||||||
|
return directory.path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final appPath = AppPath();
|
final appPath = AppPath();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
@@ -28,7 +29,8 @@ class Preferences {
|
|||||||
try {
|
try {
|
||||||
return ClashConfig.fromJson(clashConfigMap);
|
return ClashConfig.fromJson(clashConfigMap);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e.toString();
|
debugPrint(e.toString());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +50,8 @@ class Preferences {
|
|||||||
try {
|
try {
|
||||||
return Config.fromJson(configMap);
|
return Config.fromJson(configMap);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e.toString();
|
debugPrint(e.toString());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'dart:math';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/ip.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ class Request {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||||
for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
|
for (final source in _ipInfoSources.entries) {
|
||||||
try {
|
try {
|
||||||
final response = await _dio
|
final response = await _dio
|
||||||
.get<Map<String, dynamic>>(
|
.get<Map<String, dynamic>>(
|
||||||
|
|||||||
@@ -25,3 +25,18 @@ class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShowBarScrollBehavior extends BaseScrollBehavior {
|
||||||
|
@override
|
||||||
|
Widget buildScrollbar(
|
||||||
|
BuildContext context,
|
||||||
|
Widget child,
|
||||||
|
ScrollableDetails details,
|
||||||
|
) {
|
||||||
|
return Scrollbar(
|
||||||
|
interactive: true,
|
||||||
|
controller: details.controller,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
bool get isUrl {
|
bool get isUrl {
|
||||||
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
|
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
|
||||||
@@ -8,4 +13,38 @@ extension StringExtension on String {
|
|||||||
other.toLowerCase(),
|
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,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@@ -24,6 +25,16 @@ class System {
|
|||||||
return result.exitCode == 0;
|
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 {
|
back() async {
|
||||||
await app?.moveTaskToBack();
|
await app?.moveTaskToBack();
|
||||||
await window?.hide();
|
await window?.hide();
|
||||||
|
|||||||
16
lib/common/window.dart
Normal file → Executable file
16
lib/common/window.dart
Normal file → Executable file
@@ -9,7 +9,7 @@ import 'protocol.dart';
|
|||||||
import 'system.dart';
|
import 'system.dart';
|
||||||
|
|
||||||
class Window {
|
class Window {
|
||||||
init(WindowProps props) async {
|
init(WindowProps props, int version) async {
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
|
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
|
||||||
protocol.register("clash");
|
protocol.register("clash");
|
||||||
@@ -20,8 +20,6 @@ class Window {
|
|||||||
WindowOptions windowOptions = WindowOptions(
|
WindowOptions windowOptions = WindowOptions(
|
||||||
size: Size(props.width, props.height),
|
size: Size(props.width, props.height),
|
||||||
minimumSize: const Size(380, 500),
|
minimumSize: const Size(380, 500),
|
||||||
windowButtonVisibility: false,
|
|
||||||
titleBarStyle: TitleBarStyle.hidden,
|
|
||||||
);
|
);
|
||||||
if (props.left != null || props.top != null) {
|
if (props.left != null || props.top != null) {
|
||||||
await windowManager.setPosition(
|
await windowManager.setPosition(
|
||||||
@@ -30,9 +28,9 @@ class Window {
|
|||||||
} else {
|
} else {
|
||||||
await windowManager.setAlignment(Alignment.center);
|
await windowManager.setAlignment(Alignment.center);
|
||||||
}
|
}
|
||||||
// if(Platform.isWindows){
|
if(!Platform.isMacOS || version > 10){
|
||||||
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||||
// }
|
}
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
await windowManager.setPreventClose(true);
|
await windowManager.setPreventClose(true);
|
||||||
});
|
});
|
||||||
@@ -41,6 +39,11 @@ class Window {
|
|||||||
show() async {
|
show() async {
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
await windowManager.focus();
|
||||||
|
await windowManager.setSkipTaskbar(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isVisible() async {
|
||||||
|
return await windowManager.isVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
close() async {
|
close() async {
|
||||||
@@ -49,6 +52,7 @@ class Window {
|
|||||||
|
|
||||||
hide() async {
|
hide() async {
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
|
await windowManager.setSkipTaskbar(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class Windows {
|
class Windows {
|
||||||
static Windows? _instance;
|
static Windows? _instance;
|
||||||
@@ -54,6 +56,62 @@ class Windows {
|
|||||||
}
|
}
|
||||||
return true;
|
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;
|
final windows = Platform.isWindows ? Windows() : null;
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:fl_clash/common/archive.dart';
|
import 'package:fl_clash/common/archive.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'clash/core.dart';
|
import 'clash/core.dart';
|
||||||
@@ -19,6 +22,7 @@ import 'common/common.dart';
|
|||||||
class AppController {
|
class AppController {
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
late AppState appState;
|
late AppState appState;
|
||||||
|
late AppFlowingState appFlowingState;
|
||||||
late Config config;
|
late Config config;
|
||||||
late ClashConfig clashConfig;
|
late ClashConfig clashConfig;
|
||||||
late Function updateClashConfigDebounce;
|
late Function updateClashConfigDebounce;
|
||||||
@@ -30,6 +34,7 @@ class AppController {
|
|||||||
appState = context.read<AppState>();
|
appState = context.read<AppState>();
|
||||||
config = context.read<Config>();
|
config = context.read<Config>();
|
||||||
clashConfig = context.read<ClashConfig>();
|
clashConfig = context.read<ClashConfig>();
|
||||||
|
appFlowingState = context.read<AppFlowingState>();
|
||||||
updateClashConfigDebounce = debounce<Function()>(() async {
|
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||||
await updateClashConfig();
|
await updateClashConfig();
|
||||||
});
|
});
|
||||||
@@ -56,13 +61,15 @@ class AppController {
|
|||||||
updateRunTime,
|
updateRunTime,
|
||||||
updateTraffic,
|
updateTraffic,
|
||||||
];
|
];
|
||||||
applyProfileDebounce();
|
if (!Platform.isAndroid) {
|
||||||
|
applyProfileDebounce();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await globalState.handleStop();
|
await globalState.handleStop();
|
||||||
clashCore.resetTraffic();
|
clashCore.resetTraffic();
|
||||||
appState.traffics = [];
|
appFlowingState.traffics = [];
|
||||||
appState.totalTraffic = Traffic();
|
appFlowingState.totalTraffic = Traffic();
|
||||||
appState.runTime = null;
|
appFlowingState.runTime = null;
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,15 +83,15 @@ class AppController {
|
|||||||
if (startTime != null) {
|
if (startTime != null) {
|
||||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
appState.runTime = nowTimeStamp - startTimeStamp;
|
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
||||||
} else {
|
} else {
|
||||||
appState.runTime = null;
|
appFlowingState.runTime = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTraffic() {
|
updateTraffic() {
|
||||||
globalState.updateTraffic(
|
globalState.updateTraffic(
|
||||||
appState: appState,
|
appFlowingState: appFlowingState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +109,7 @@ class AppController {
|
|||||||
final updateId = config.profiles.first.id;
|
final updateId = config.profiles.first.id;
|
||||||
changeProfile(updateId);
|
changeProfile(updateId);
|
||||||
} else {
|
} else {
|
||||||
|
changeProfile(null);
|
||||||
updateStatus(false);
|
updateStatus(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,11 +123,15 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||||
await globalState.updateClashConfig(
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||||
clashConfig: clashConfig,
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
config: config,
|
await commonScaffoldState?.loadingRun(() async {
|
||||||
isPatch: isPatch,
|
await globalState.updateClashConfig(
|
||||||
);
|
clashConfig: clashConfig,
|
||||||
|
config: config,
|
||||||
|
isPatch: isPatch,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future applyProfile({bool isPrue = false}) async {
|
Future applyProfile({bool isPrue = false}) async {
|
||||||
@@ -162,7 +174,7 @@ class AppController {
|
|||||||
try {
|
try {
|
||||||
updateProfile(profile);
|
updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appState.addLog(
|
appFlowingState.addLog(
|
||||||
Log(
|
Log(
|
||||||
logLevel: LogLevel.info,
|
logLevel: LogLevel.info,
|
||||||
payload: e.toString(),
|
payload: e.toString(),
|
||||||
@@ -217,7 +229,7 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleBackOrExit() async {
|
handleBackOrExit() async {
|
||||||
if (config.isMinimizeOnExit) {
|
if (config.appSetting.minimizeOnExit) {
|
||||||
if (system.isDesktop) {
|
if (system.isDesktop) {
|
||||||
await savePreferences();
|
await savePreferences();
|
||||||
}
|
}
|
||||||
@@ -236,16 +248,16 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLogStatus() {
|
updateLogStatus() {
|
||||||
if (config.openLogs) {
|
if (config.appSetting.openLogs) {
|
||||||
clashCore.startLog();
|
clashCore.startLog();
|
||||||
} else {
|
} else {
|
||||||
clashCore.stopLog();
|
clashCore.stopLog();
|
||||||
appState.logs = [];
|
appFlowingState.logs = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
autoCheckUpdate() async {
|
autoCheckUpdate() async {
|
||||||
if (!config.autoCheckUpdate) return;
|
if (!config.appSetting.autoCheckUpdate) return;
|
||||||
final res = await request.checkForUpdate();
|
final res = await request.checkForUpdate();
|
||||||
checkUpdateResultHandle(data: res);
|
checkUpdateResultHandle(data: res);
|
||||||
}
|
}
|
||||||
@@ -294,8 +306,12 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() async {
|
init() async {
|
||||||
|
final isDisclaimerAccepted = await handlerDisclaimer();
|
||||||
|
if (!isDisclaimerAccepted) {
|
||||||
|
handleExit();
|
||||||
|
}
|
||||||
updateLogStatus();
|
updateLogStatus();
|
||||||
if (!config.silentLaunch) {
|
if (!config.appSetting.silentLaunch) {
|
||||||
window?.show();
|
window?.show();
|
||||||
}
|
}
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -304,20 +320,12 @@ class AppController {
|
|||||||
if (globalState.isStart) {
|
if (globalState.isStart) {
|
||||||
await updateStatus(true);
|
await updateStatus(true);
|
||||||
} else {
|
} else {
|
||||||
await updateStatus(config.autoRun);
|
await updateStatus(config.appSetting.autoRun);
|
||||||
}
|
}
|
||||||
autoUpdateProfiles();
|
autoUpdateProfiles();
|
||||||
autoCheckUpdate();
|
autoCheckUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTray() {
|
|
||||||
globalState.updateTray(
|
|
||||||
appState: appState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDelay(Delay delay) {
|
setDelay(Delay delay) {
|
||||||
appState.setDelay(delay);
|
appState.setDelay(delay);
|
||||||
}
|
}
|
||||||
@@ -327,7 +335,7 @@ class AppController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appState.currentLabel = appState.currentNavigationItems[index].label;
|
appState.currentLabel = appState.currentNavigationItems[index].label;
|
||||||
if ((config.isAnimateToPage || hasAnimate)) {
|
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
|
||||||
globalState.pageController?.animateToPage(
|
globalState.pageController?.animateToPage(
|
||||||
index,
|
index,
|
||||||
duration: kTabScrollDuration,
|
duration: kTabScrollDuration,
|
||||||
@@ -380,6 +388,49 @@ class AppController {
|
|||||||
globalState.showSnackBar(context, message: 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 {
|
addProfileFormURL(String url) async {
|
||||||
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
|
if (globalState.navigatorKey.currentState?.canPop() ?? false) {
|
||||||
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
||||||
@@ -466,7 +517,7 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
List<Proxy> getSortProxies(List<Proxy> proxies) {
|
||||||
return switch (config.proxiesSortType) {
|
return switch (config.proxiesStyle.sortType) {
|
||||||
ProxiesSortType.none => proxies,
|
ProxiesSortType.none => proxies,
|
||||||
ProxiesSortType.delay => _sortOfDelay(proxies),
|
ProxiesSortType.delay => _sortOfDelay(proxies),
|
||||||
ProxiesSortType.name => _sortOfName(proxies),
|
ProxiesSortType.name => _sortOfName(proxies),
|
||||||
@@ -480,6 +531,67 @@ class AppController {
|
|||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTun() {
|
||||||
|
clashConfig.tun = clashConfig.tun.copyWith(
|
||||||
|
enable: !clashConfig.tun.enable,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSystemProxy() {
|
||||||
|
config.desktopProps = config.desktopProps.copyWith(
|
||||||
|
systemProxy: !config.desktopProps.systemProxy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStart() {
|
||||||
|
updateStatus(!appFlowingState.isStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAutoLaunch() {
|
||||||
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
autoLaunch: !config.appSetting.autoLaunch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAdminAutoLaunch() {
|
||||||
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
adminAutoLaunch: !config.appSetting.adminAutoLaunch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVisible() async {
|
||||||
|
final visible = await window?.isVisible();
|
||||||
|
if (visible != null && !visible) {
|
||||||
|
window?.show();
|
||||||
|
} else {
|
||||||
|
window?.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMode() {
|
||||||
|
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
|
||||||
|
if (index == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||||
|
clashConfig.mode = Mode.values[nextIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> exportLogs() async {
|
||||||
|
final logsRaw = appFlowingState.logs.map(
|
||||||
|
(item) => item.toString(),
|
||||||
|
);
|
||||||
|
final data = await Isolate.run<List<int>>(() async {
|
||||||
|
final logsRawString = logsRaw.join("\n");
|
||||||
|
return utf8.encode(logsRawString);
|
||||||
|
});
|
||||||
|
return await picker.saveFile(
|
||||||
|
other.logFile,
|
||||||
|
Uint8List.fromList(data),
|
||||||
|
) !=
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<int>> backupData() async {
|
Future<List<int>> backupData() async {
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
final homeDirPath = await appPath.getHomeDirPath();
|
||||||
final profilesPath = await appPath.getProfilesPath();
|
final profilesPath = await appPath.getProfilesPath();
|
||||||
@@ -495,6 +607,117 @@ class AppController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _updateSystemTray({
|
||||||
|
required bool isStart,
|
||||||
|
required Brightness? brightness,
|
||||||
|
}) async {
|
||||||
|
await trayManager.destroy();
|
||||||
|
await trayManager.setIcon(
|
||||||
|
other.getTrayIconPath(
|
||||||
|
isStart: isStart,
|
||||||
|
brightness: brightness ??
|
||||||
|
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
await trayManager.setToolTip(
|
||||||
|
appName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTray() async {
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
await _updateSystemTray(
|
||||||
|
isStart: appFlowingState.isStart,
|
||||||
|
brightness: appState.brightness,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<MenuItem> menuItems = [];
|
||||||
|
final showMenuItem = MenuItem(
|
||||||
|
label: appLocalizations.show,
|
||||||
|
onClick: (_) {
|
||||||
|
window?.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
menuItems.add(showMenuItem);
|
||||||
|
final startMenuItem = MenuItem.checkbox(
|
||||||
|
label:
|
||||||
|
appFlowingState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||||
|
onClick: (_) async {
|
||||||
|
globalState.appController.updateStart();
|
||||||
|
},
|
||||||
|
checked: false,
|
||||||
|
);
|
||||||
|
menuItems.add(startMenuItem);
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
for (final mode in Mode.values) {
|
||||||
|
menuItems.add(
|
||||||
|
MenuItem.checkbox(
|
||||||
|
label: Intl.message(mode.name),
|
||||||
|
onClick: (_) {
|
||||||
|
globalState.appController.clashConfig.mode = mode;
|
||||||
|
},
|
||||||
|
checked: mode == clashConfig.mode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
if (appFlowingState.isStart) {
|
||||||
|
menuItems.add(
|
||||||
|
MenuItem.checkbox(
|
||||||
|
label: appLocalizations.tun,
|
||||||
|
onClick: (_) {
|
||||||
|
globalState.appController.updateTun();
|
||||||
|
},
|
||||||
|
checked: clashConfig.tun.enable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
menuItems.add(
|
||||||
|
MenuItem.checkbox(
|
||||||
|
label: appLocalizations.systemProxy,
|
||||||
|
onClick: (_) {
|
||||||
|
globalState.appController.updateSystemProxy();
|
||||||
|
},
|
||||||
|
checked: config.desktopProps.systemProxy,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
}
|
||||||
|
final autoStartMenuItem = MenuItem.checkbox(
|
||||||
|
label: appLocalizations.autoLaunch,
|
||||||
|
onClick: (_) async {
|
||||||
|
globalState.appController.updateAutoLaunch();
|
||||||
|
},
|
||||||
|
checked: config.appSetting.autoLaunch,
|
||||||
|
);
|
||||||
|
final adminAutoStartMenuItem = MenuItem.checkbox(
|
||||||
|
label: appLocalizations.adminAutoLaunch,
|
||||||
|
onClick: (_) async {
|
||||||
|
globalState.appController.updateAdminAutoLaunch();
|
||||||
|
},
|
||||||
|
checked: config.appSetting.adminAutoLaunch,
|
||||||
|
);
|
||||||
|
menuItems.add(autoStartMenuItem);
|
||||||
|
menuItems.add(adminAutoStartMenuItem);
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
final exitMenuItem = MenuItem(
|
||||||
|
label: appLocalizations.exit,
|
||||||
|
onClick: (_) async {
|
||||||
|
await globalState.appController.handleExit();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
menuItems.add(exitMenuItem);
|
||||||
|
final menu = Menu(items: menuItems);
|
||||||
|
await trayManager.setContextMenu(menu);
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
await _updateSystemTray(
|
||||||
|
isStart: appFlowingState.isStart,
|
||||||
|
brightness: appState.brightness,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recoveryData(
|
recoveryData(
|
||||||
List<int> data,
|
List<int> data,
|
||||||
RecoveryOption recoveryOption,
|
RecoveryOption recoveryOption,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// ignore_for_file: constant_identifier_names
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
|
|
||||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
||||||
|
|
||||||
@@ -13,6 +15,10 @@ extension GroupTypeExtension on GroupType {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
bool get isURLTestOrFallback {
|
||||||
|
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||||
|
}
|
||||||
|
|
||||||
static GroupType? getGroupType(String value) {
|
static GroupType? getGroupType(String value) {
|
||||||
final index = GroupTypeExtension.valueList.indexOf(value);
|
final index = GroupTypeExtension.valueList.indexOf(value);
|
||||||
if (index == -1) return null;
|
if (index == -1) return null;
|
||||||
@@ -92,7 +98,6 @@ enum ProxiesLayout { loose, standard, tight }
|
|||||||
|
|
||||||
enum ProxyCardType { expand, shrink, min }
|
enum ProxyCardType { expand, shrink, min }
|
||||||
|
|
||||||
|
|
||||||
enum DnsMode {
|
enum DnsMode {
|
||||||
normal,
|
normal,
|
||||||
@JsonValue("fake-ip")
|
@JsonValue("fake-ip")
|
||||||
@@ -102,3 +107,58 @@ enum DnsMode {
|
|||||||
hosts
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class AboutFragment extends StatelessWidget {
|
|||||||
title: const Text("Telegram"),
|
title: const Text("Telegram"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
globalState.openUrl(
|
globalState.openUrl(
|
||||||
"https://t.me/+G-veVtwBOl4wODc1",
|
"https://t.me/FlClash",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
trailing: const Icon(Icons.launch),
|
trailing: const Icon(Icons.launch),
|
||||||
|
|||||||
@@ -8,6 +8,84 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.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 {
|
class ApplicationSettingFragment extends StatelessWidget {
|
||||||
const ApplicationSettingFragment({super.key});
|
const ApplicationSettingFragment({super.key});
|
||||||
|
|
||||||
@@ -20,17 +98,18 @@ class ApplicationSettingFragment extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> items = [
|
List<Widget> items = [
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.isMinimizeOnExit,
|
selector: (_, config) => config.appSetting.minimizeOnExit,
|
||||||
builder: (_, isMinimizeOnExit, child) {
|
builder: (_, isMinimizeOnExit, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.back_hand),
|
|
||||||
title: Text(appLocalizations.minimizeOnExit),
|
title: Text(appLocalizations.minimizeOnExit),
|
||||||
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: isMinimizeOnExit,
|
value: isMinimizeOnExit,
|
||||||
onChanged: (bool value) {
|
onChanged: (bool value) {
|
||||||
final config = context.read<Config>();
|
final config = context.read<Config>();
|
||||||
config.isMinimizeOnExit = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
minimizeOnExit: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -38,52 +117,57 @@ class ApplicationSettingFragment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (system.isDesktop)
|
if (system.isDesktop)
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.autoLaunch,
|
selector: (_, config) => config.appSetting.autoLaunch,
|
||||||
builder: (_, autoLaunch, child) {
|
builder: (_, autoLaunch, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.rocket_launch),
|
|
||||||
title: Text(appLocalizations.autoLaunch),
|
title: Text(appLocalizations.autoLaunch),
|
||||||
subtitle: Text(appLocalizations.autoLaunchDesc),
|
subtitle: Text(appLocalizations.autoLaunchDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: autoLaunch,
|
value: autoLaunch,
|
||||||
onChanged: (bool value) {
|
onChanged: (bool value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.autoLaunch = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
autoLaunch: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if(Platform.isWindows)
|
||||||
|
const AdminAutoLaunchItem(),
|
||||||
if (system.isDesktop)
|
if (system.isDesktop)
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.silentLaunch,
|
selector: (_, config) => config.appSetting.silentLaunch,
|
||||||
builder: (_, silentLaunch, child) {
|
builder: (_, silentLaunch, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.expand_circle_down),
|
|
||||||
title: Text(appLocalizations.silentLaunch),
|
title: Text(appLocalizations.silentLaunch),
|
||||||
subtitle: Text(appLocalizations.silentLaunchDesc),
|
subtitle: Text(appLocalizations.silentLaunchDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: silentLaunch,
|
value: silentLaunch,
|
||||||
onChanged: (bool value) {
|
onChanged: (bool value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.silentLaunch = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
silentLaunch: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.autoRun,
|
selector: (_, config) => config.appSetting.autoRun,
|
||||||
builder: (_, autoRun, child) {
|
builder: (_, autoRun, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.not_started),
|
|
||||||
title: Text(appLocalizations.autoRun),
|
title: Text(appLocalizations.autoRun),
|
||||||
subtitle: Text(appLocalizations.autoRunDesc),
|
subtitle: Text(appLocalizations.autoRunDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: autoRun,
|
value: autoRun,
|
||||||
onChanged: (bool value) {
|
onChanged: (bool value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.autoRun = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
autoRun: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -91,17 +175,18 @@ class ApplicationSettingFragment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.isExclude,
|
selector: (_, config) => config.appSetting.hidden,
|
||||||
builder: (_, isExclude, child) {
|
builder: (_, isExclude, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.visibility_off),
|
|
||||||
title: Text(appLocalizations.exclude),
|
title: Text(appLocalizations.exclude),
|
||||||
subtitle: Text(appLocalizations.excludeDesc),
|
subtitle: Text(appLocalizations.excludeDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: isExclude,
|
value: isExclude,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.isExclude = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
hidden: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -109,52 +194,56 @@ class ApplicationSettingFragment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.isAnimateToPage,
|
selector: (_, config) => config.appSetting.isAnimateToPage,
|
||||||
builder: (_, isAnimateToPage, child) {
|
builder: (_, isAnimateToPage, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.animation),
|
|
||||||
title: Text(appLocalizations.tabAnimation),
|
title: Text(appLocalizations.tabAnimation),
|
||||||
subtitle: Text(appLocalizations.tabAnimationDesc),
|
subtitle: Text(appLocalizations.tabAnimationDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: isAnimateToPage,
|
value: isAnimateToPage,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.isAnimateToPage = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
isAnimateToPage: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.openLogs,
|
selector: (_, config) => config.appSetting.openLogs,
|
||||||
builder: (_, openLogs, child) {
|
builder: (_, openLogs, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.bug_report),
|
|
||||||
title: Text(appLocalizations.logcat),
|
title: Text(appLocalizations.logcat),
|
||||||
subtitle: Text(appLocalizations.logcatDesc),
|
subtitle: Text(appLocalizations.logcatDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: openLogs,
|
value: openLogs,
|
||||||
onChanged: (bool value) {
|
onChanged: (bool value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.openLogs = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
globalState.appController.updateLogStatus();
|
openLogs: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const CloseConnectionsSwitch(),
|
||||||
|
const UsageSwitch(),
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.autoCheckUpdate,
|
selector: (_, config) => config.appSetting.autoCheckUpdate,
|
||||||
builder: (_, autoCheckUpdate, child) {
|
builder: (_, autoCheckUpdate, child) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
leading: const Icon(Icons.system_update),
|
|
||||||
title: Text(appLocalizations.autoCheckUpdate),
|
title: Text(appLocalizations.autoCheckUpdate),
|
||||||
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
|
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: autoCheckUpdate,
|
value: autoCheckUpdate,
|
||||||
onChanged: (bool value) {
|
onChanged: (bool value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.autoCheckUpdate = value;
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
autoCheckUpdate: value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/common/dav_client.dart';
|
import 'package:fl_clash/common/dav_client.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/config.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/models/dav.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/fade_box.dart';
|
import 'package:fl_clash/widgets/fade_box.dart';
|
||||||
import 'package:fl_clash/widgets/list.dart';
|
import 'package:fl_clash/widgets/list.dart';
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/config.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:path/path.dart' show dirname, join;
|
|
||||||
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.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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UsageSwitch extends StatelessWidget {
|
|
||||||
const UsageSwitch({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return 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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UWPLoopbackUtil extends StatelessWidget {
|
|
||||||
const UWPLoopbackUtil({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.onlyProxy,
|
|
||||||
builder: (_, onlyProxy, __) {
|
|
||||||
return ListItem(
|
|
||||||
leading: const Icon(Icons.lock_open),
|
|
||||||
title: Text(appLocalizations.loopback),
|
|
||||||
subtitle: Text(appLocalizations.loopbackDesc),
|
|
||||||
onTap: () {
|
|
||||||
windows?.runas(
|
|
||||||
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final appItems = [
|
|
||||||
if (Platform.isWindows) const UWPLoopbackUtil(),
|
|
||||||
const CloseConnectionsSwitch(),
|
|
||||||
const UsageSwitch(),
|
|
||||||
];
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/fragments/config/app.dart';
|
|
||||||
import 'package:fl_clash/fragments/config/dns.dart';
|
import 'package:fl_clash/fragments/config/dns.dart';
|
||||||
import 'package:fl_clash/fragments/config/general.dart';
|
import 'package:fl_clash/fragments/config/general.dart';
|
||||||
import 'package:fl_clash/fragments/config/vpn.dart';
|
import 'package:fl_clash/fragments/config/network.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@@ -20,36 +17,16 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> items = [
|
List<Widget> items = [
|
||||||
ListItem.open(
|
ListItem.open(
|
||||||
title: Text(appLocalizations.app),
|
title: Text(appLocalizations.network),
|
||||||
subtitle: Text(appLocalizations.appDesc),
|
subtitle: Text(appLocalizations.networkDesc),
|
||||||
leading: const Icon(Icons.settings_applications),
|
leading: const Icon(Icons.vpn_key),
|
||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
title: appLocalizations.app,
|
title: appLocalizations.network,
|
||||||
|
isScaffold: true,
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
widget: generateListView(
|
widget: const NetworkListView(),
|
||||||
appItems
|
|
||||||
.separated(
|
|
||||||
const Divider(
|
|
||||||
height: 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (Platform.isAndroid)
|
|
||||||
ListItem.open(
|
|
||||||
title: const Text("VPN"),
|
|
||||||
subtitle: Text(appLocalizations.vpnDesc),
|
|
||||||
leading: const Icon(Icons.vpn_key),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: "VPN",
|
|
||||||
isBlur: false,
|
|
||||||
widget: generateListView(
|
|
||||||
vpnItems,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListItem.open(
|
ListItem.open(
|
||||||
title: Text(appLocalizations.general),
|
title: Text(appLocalizations.general),
|
||||||
subtitle: Text(appLocalizations.generalDesc),
|
subtitle: Text(appLocalizations.generalDesc),
|
||||||
@@ -67,11 +44,9 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
title: const Text("DNS"),
|
title: const Text("DNS"),
|
||||||
subtitle: Text(appLocalizations.dnsDesc),
|
subtitle: Text(appLocalizations.dnsDesc),
|
||||||
leading: const Icon(Icons.dns),
|
leading: const Icon(Icons.dns),
|
||||||
delegate: OpenDelegate(
|
delegate: const OpenDelegate(
|
||||||
title: "DNS",
|
title: "DNS",
|
||||||
widget: generateListView(
|
widget: DnsListView(),
|
||||||
dnsItems,
|
|
||||||
),
|
|
||||||
isScaffold: true,
|
isScaffold: true,
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fl_clash/common/app_localizations.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
@@ -11,27 +10,8 @@ import 'package:provider/provider.dart';
|
|||||||
class OverrideItem extends StatelessWidget {
|
class OverrideItem extends StatelessWidget {
|
||||||
const OverrideItem({super.key});
|
const OverrideItem({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
globalState.appController.clashConfig.dns = const Dns();
|
|
||||||
},
|
|
||||||
tooltip: appLocalizations.resetDns,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.replay,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_initActions(context);
|
|
||||||
return Selector<Config, bool>(
|
return Selector<Config, bool>(
|
||||||
selector: (_, config) => config.overrideDns,
|
selector: (_, config) => config.overrideDns,
|
||||||
builder: (_, override, __) {
|
builder: (_, override, __) {
|
||||||
@@ -51,34 +31,6 @@ class OverrideItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DnsDisabledContainer extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const DnsDisabledContainer(this.child, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.overrideDns,
|
|
||||||
builder: (_, enable, child) {
|
|
||||||
return AbsorbPointer(
|
|
||||||
absorbing: !enable,
|
|
||||||
child: DisabledMask(
|
|
||||||
status: !enable,
|
|
||||||
child: Container(
|
|
||||||
color: context.colorScheme.surface,
|
|
||||||
child: child!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusItem extends StatelessWidget {
|
class StatusItem extends StatelessWidget {
|
||||||
const StatusItem({super.key});
|
const StatusItem({super.key});
|
||||||
|
|
||||||
@@ -267,28 +219,17 @@ class FakeIpFilterItem extends StatelessWidget {
|
|||||||
title: appLocalizations.fakeipFilter,
|
title: appLocalizations.fakeipFilter,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, fakeIpFilter, __) {
|
builder: (_, fakeIpFilter, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.fakeipFilter,
|
title: appLocalizations.fakeipFilter,
|
||||||
items: fakeIpFilter,
|
items: fakeIpFilter,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
fakeIpFilter: List.from(dns.fakeIpFilter)
|
fakeIpFilter: List.from(items),
|
||||||
..remove(value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
if (fakeIpFilter.contains(value)) return;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
fakeIpFilter: List.from(dns.fakeIpFilter)
|
|
||||||
..add(value),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -313,28 +254,17 @@ class DefaultNameserverItem extends StatelessWidget {
|
|||||||
title: appLocalizations.defaultNameserver,
|
title: appLocalizations.defaultNameserver,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, defaultNameserver, __) {
|
builder: (_, defaultNameserver, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.defaultNameserver,
|
title: appLocalizations.defaultNameserver,
|
||||||
items: defaultNameserver,
|
items: defaultNameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
defaultNameserver: List.from(dns.defaultNameserver)
|
defaultNameserver: List.from(items),
|
||||||
..remove(value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
if (defaultNameserver.contains(value)) return;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
defaultNameserver: List.from(dns.defaultNameserver)
|
|
||||||
..add(value),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -359,28 +289,17 @@ class NameserverItem extends StatelessWidget {
|
|||||||
isBlur: false,
|
isBlur: false,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, nameserver, __) {
|
builder: (_, nameserver, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: "域名服务器",
|
title: "域名服务器",
|
||||||
items: nameserver,
|
items: nameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
nameserver: List.from(dns.nameserver)
|
nameserver: List.from(items),
|
||||||
..remove(value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
if (nameserver.contains(value)) return;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
nameserver: List.from(dns.nameserver)
|
|
||||||
..add(value),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -458,28 +377,18 @@ class NameserverPolicyItem extends StatelessWidget {
|
|||||||
widget: Selector<ClashConfig, Map<String, String>>(
|
widget: Selector<ClashConfig, Map<String, String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) =>
|
||||||
!const MapEquality<String, String>().equals(prev, next),
|
!const MapEquality<String, String>().equals(prev, next),
|
||||||
builder: (_, nameserverPolicy, __) {
|
builder: (_, nameserverPolicy, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.nameserverPolicy,
|
title: appLocalizations.nameserverPolicy,
|
||||||
items: nameserverPolicy.entries,
|
items: nameserverPolicy.entries,
|
||||||
titleBuilder: (item) => Text(item.key),
|
titleBuilder: (item) => Text(item.key),
|
||||||
subtitleBuilder: (item) => Text(item.value),
|
subtitleBuilder: (item) => Text(item.value),
|
||||||
isMap: true,
|
onChange: (items){
|
||||||
onRemove: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
nameserverPolicy: Map.from(dns.nameserverPolicy)
|
nameserverPolicy: Map.fromEntries(items),
|
||||||
..remove(value.key),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
nameserverPolicy: Map.from(dns.nameserverPolicy)
|
|
||||||
..addEntries([value]),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -504,28 +413,17 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
|||||||
title: appLocalizations.proxyNameserver,
|
title: appLocalizations.proxyNameserver,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, proxyServerNameserver, __) {
|
builder: (_, proxyServerNameserver, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.proxyNameserver,
|
title: appLocalizations.proxyNameserver,
|
||||||
items: proxyServerNameserver,
|
items: proxyServerNameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
proxyServerNameserver: List.from(dns.proxyServerNameserver)
|
proxyServerNameserver: List.from(items),
|
||||||
..remove(value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
if (proxyServerNameserver.contains(value)) return;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
proxyServerNameserver: List.from(dns.proxyServerNameserver)
|
|
||||||
..add(value),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -550,28 +448,17 @@ class FallbackItem extends StatelessWidget {
|
|||||||
title: appLocalizations.fallback,
|
title: appLocalizations.fallback,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, fallback, __) {
|
builder: (_, fallback, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.fallback,
|
title: appLocalizations.fallback,
|
||||||
items: fallback,
|
items: fallback,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
fallback: List.from(dns.fallback)
|
fallback: List.from(items),
|
||||||
..remove(value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
if (fallback.contains(value)) return;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
fallback: List.from(dns.fallback)
|
|
||||||
..add(value),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -662,30 +549,18 @@ class GeositeItem extends StatelessWidget {
|
|||||||
title: "Geosite",
|
title: "Geosite",
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, geosite, __) {
|
builder: (_, geosite, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: "Geosite",
|
title: "Geosite",
|
||||||
items: geosite,
|
items: geosite,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||||
geosite: List.from(geosite)
|
geosite: List.from(items),
|
||||||
..remove(value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
|
||||||
geosite: List.from(geosite)
|
|
||||||
..add(value),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -710,30 +585,18 @@ class IpcidrItem extends StatelessWidget {
|
|||||||
title: appLocalizations.ipcidr,
|
title: appLocalizations.ipcidr,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, ipcidr, __) {
|
builder: (_, ipcidr, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.ipcidr,
|
title: appLocalizations.ipcidr,
|
||||||
items: ipcidr,
|
items: ipcidr,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||||
ipcidr: List.from(ipcidr)
|
ipcidr: List.from(items),
|
||||||
..remove(value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
|
||||||
ipcidr: List.from(ipcidr)
|
|
||||||
..add(value),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -758,30 +621,18 @@ class DomainItem extends StatelessWidget {
|
|||||||
title: appLocalizations.domain,
|
title: appLocalizations.domain,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Selector<ClashConfig, List<String>>(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
||||||
shouldRebuild: (prev, next) =>
|
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||||
!const ListEquality<String>().equals(prev, next),
|
|
||||||
builder: (_, domain, __) {
|
builder: (_, domain, __) {
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: appLocalizations.domain,
|
title: appLocalizations.domain,
|
||||||
items: domain,
|
items: domain,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
final dns = clashConfig.dns;
|
final dns = clashConfig.dns;
|
||||||
clashConfig.dns = dns.copyWith(
|
clashConfig.dns = dns.copyWith(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||||
domain: List.from(domain)
|
domain: List.from(items),
|
||||||
..remove(value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
final dns = clashConfig.dns;
|
|
||||||
clashConfig.dns = dns.copyWith(
|
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
|
||||||
domain: List.from(domain)
|
|
||||||
..add(value),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -799,27 +650,25 @@ class DnsOptions extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DnsDisabledContainer(
|
return Column(
|
||||||
Column(
|
children: generateSection(
|
||||||
children: generateSection(
|
title: appLocalizations.options,
|
||||||
title: appLocalizations.options,
|
items: [
|
||||||
items: [
|
const StatusItem(),
|
||||||
const StatusItem(),
|
const UseHostsItem(),
|
||||||
const UseHostsItem(),
|
const UseSystemHostsItem(),
|
||||||
const UseSystemHostsItem(),
|
const IPv6Item(),
|
||||||
const IPv6Item(),
|
const RespectRulesItem(),
|
||||||
const RespectRulesItem(),
|
const PreferH3Item(),
|
||||||
const PreferH3Item(),
|
const DnsModeItem(),
|
||||||
const DnsModeItem(),
|
const FakeIpRangeItem(),
|
||||||
const FakeIpRangeItem(),
|
const FakeIpFilterItem(),
|
||||||
const FakeIpFilterItem(),
|
const DefaultNameserverItem(),
|
||||||
const DefaultNameserverItem(),
|
const NameserverPolicyItem(),
|
||||||
const NameserverPolicyItem(),
|
const NameserverItem(),
|
||||||
const NameserverItem(),
|
const FallbackItem(),
|
||||||
const FallbackItem(),
|
const ProxyServerNameserverItem(),
|
||||||
const ProxyServerNameserverItem(),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -830,18 +679,16 @@ class FallbackFilterOptions extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DnsDisabledContainer(
|
return Column(
|
||||||
Column(
|
children: generateSection(
|
||||||
children: generateSection(
|
title: appLocalizations.fallbackFilter,
|
||||||
title: appLocalizations.fallbackFilter,
|
items: [
|
||||||
items: [
|
const GeoipItem(),
|
||||||
const GeoipItem(),
|
const GeoipCodeItem(),
|
||||||
const GeoipCodeItem(),
|
const GeositeItem(),
|
||||||
const GeositeItem(),
|
const IpcidrItem(),
|
||||||
const IpcidrItem(),
|
const DomainItem(),
|
||||||
const DomainItem(),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -852,3 +699,41 @@ const dnsItems = <Widget>[
|
|||||||
DnsOptions(),
|
DnsOptions(),
|
||||||
FallbackFilterOptions(),
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class TestUrlItem extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Config, String>(
|
return Selector<Config, String>(
|
||||||
selector: (_, config) => config.testUrl,
|
selector: (_, config) => config.appSetting.testUrl,
|
||||||
builder: (_, value, __) {
|
builder: (_, value, __) {
|
||||||
return ListItem.input(
|
return ListItem.input(
|
||||||
leading: const Icon(Icons.timeline),
|
leading: const Icon(Icons.timeline),
|
||||||
@@ -135,7 +135,10 @@ class TestUrlItem extends StatelessWidget {
|
|||||||
if (!value.isUrl) {
|
if (!value.isUrl) {
|
||||||
throw "Invalid url";
|
throw "Invalid url";
|
||||||
}
|
}
|
||||||
globalState.appController.config.testUrl = value;
|
final config = globalState.appController.config;
|
||||||
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
testUrl: value,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
globalState.showMessage(
|
globalState.showMessage(
|
||||||
title: appLocalizations.testUrl,
|
title: appLocalizations.testUrl,
|
||||||
@@ -212,21 +215,15 @@ class HostsItem extends StatelessWidget {
|
|||||||
!const MapEquality<String, String>().equals(prev, next),
|
!const MapEquality<String, String>().equals(prev, next),
|
||||||
builder: (_, hosts, ___) {
|
builder: (_, hosts, ___) {
|
||||||
final entries = hosts.entries;
|
final entries = hosts.entries;
|
||||||
return UpdatePage(
|
return ListPage(
|
||||||
title: "Hosts",
|
title: "Hosts",
|
||||||
items: entries,
|
items: entries,
|
||||||
titleBuilder: (item) => Text(item.key),
|
titleBuilder: (item) => Text(item.key),
|
||||||
subtitleBuilder: (item) => Text(item.value),
|
subtitleBuilder: (item) => Text(item.value),
|
||||||
onRemove: (value) {
|
onChange: (items){
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
clashConfig.hosts = Map.from(hosts)..remove(value.key);
|
clashConfig.hosts = Map.fromEntries(items);
|
||||||
},
|
},
|
||||||
onAdd: (value) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
clashConfig.hosts = Map.from(clashConfig.hosts)
|
|
||||||
..addEntries([value]);
|
|
||||||
},
|
|
||||||
isMap: true,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
270
lib/fragments/config/network.dart
Normal file
270
lib/fragments/config/network.dart
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class VPNSwitch extends StatelessWidget {
|
||||||
|
const VPNSwitch({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.vpnProps.enable,
|
||||||
|
builder: (_, enable, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: const Text("VPN"),
|
||||||
|
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: enable,
|
||||||
|
onChanged: (value) async {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
config.vpnProps = config.vpnProps.copyWith(
|
||||||
|
enable: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TUNItem extends StatelessWidget {
|
||||||
|
const TUNItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.vpnProps.enable,
|
||||||
|
builder: (_, enable, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.tun),
|
||||||
|
subtitle: Text(appLocalizations.tunDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: enable,
|
||||||
|
onChanged: (value) async {
|
||||||
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
|
clashConfig.tun = clashConfig.tun.copyWith(
|
||||||
|
enable: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AllowBypassSwitch extends StatelessWidget {
|
||||||
|
const AllowBypassSwitch({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.vpnProps.allowBypass,
|
||||||
|
builder: (_, allowBypass, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.allowBypass),
|
||||||
|
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: allowBypass,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
final vpnProps = config.vpnProps;
|
||||||
|
config.vpnProps = vpnProps.copyWith(
|
||||||
|
allowBypass: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemProxySwitch extends StatelessWidget {
|
||||||
|
const SystemProxySwitch({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.vpnProps.systemProxy,
|
||||||
|
builder: (_, systemProxy, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.systemProxy),
|
||||||
|
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: systemProxy,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
final vpnProps = config.vpnProps;
|
||||||
|
config.vpnProps = vpnProps.copyWith(
|
||||||
|
systemProxy: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ipv6Switch extends StatelessWidget {
|
||||||
|
const Ipv6Switch({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.vpnProps.ipv6,
|
||||||
|
builder: (_, ipv6, __) {
|
||||||
|
return ListItem.switchItem(
|
||||||
|
title: const Text("IPv6"),
|
||||||
|
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: ipv6,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
final vpnProps = config.vpnProps;
|
||||||
|
config.vpnProps = vpnProps.copyWith(
|
||||||
|
ipv6: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TunStackItem extends StatelessWidget {
|
||||||
|
const TunStackItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<ClashConfig, TunStack>(
|
||||||
|
selector: (_, clashConfig) => clashConfig.tun.stack,
|
||||||
|
builder: (_, stack, __) {
|
||||||
|
return ListItem.options(
|
||||||
|
title: Text(appLocalizations.stackMode),
|
||||||
|
subtitle: Text(stack.name),
|
||||||
|
delegate: OptionsDelegate<TunStack>(
|
||||||
|
value: stack,
|
||||||
|
options: TunStack.values,
|
||||||
|
textBuilder: (value) => value.name,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
|
clashConfig.tun = clashConfig.tun.copyWith(
|
||||||
|
stack: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: appLocalizations.stackMode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BypassDomainItem extends StatelessWidget {
|
||||||
|
const BypassDomainItem({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
title: Text(appLocalizations.bypassDomain),
|
||||||
|
subtitle: Text(appLocalizations.bypassDomainDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
isBlur: false,
|
||||||
|
title: appLocalizations.bypassDomain,
|
||||||
|
widget: Selector<Config, List<String>>(
|
||||||
|
selector: (_, config) => config.vpnProps.bypassDomain,
|
||||||
|
shouldRebuild: (prev, next) =>
|
||||||
|
!stringListEquality.equals(prev, next),
|
||||||
|
builder: (_, bypassDomain, __) {
|
||||||
|
return ListPage(
|
||||||
|
title: appLocalizations.bypassDomain,
|
||||||
|
items: bypassDomain,
|
||||||
|
titleBuilder: (item) => Text(item),
|
||||||
|
onChange: (items){
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
config.vpnProps = config.vpnProps.copyWith(
|
||||||
|
bypassDomain: List.from(items),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
extendPageWidth: 360,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final networkItems = [
|
||||||
|
Platform.isAndroid ? const VPNSwitch() : const TUNItem(),
|
||||||
|
if (Platform.isAndroid)
|
||||||
|
...generateSection(
|
||||||
|
title: "VPN",
|
||||||
|
items: [
|
||||||
|
const SystemProxySwitch(),
|
||||||
|
const AllowBypassSwitch(),
|
||||||
|
const Ipv6Switch(),
|
||||||
|
const BypassDomainItem(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...generateSection(
|
||||||
|
title: appLocalizations.options,
|
||||||
|
items: [
|
||||||
|
const TunStackItem(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
class NetworkListView extends StatelessWidget {
|
||||||
|
const NetworkListView({super.key});
|
||||||
|
|
||||||
|
_initActions(BuildContext context) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
final commonScaffoldState =
|
||||||
|
context.findAncestorStateOfType<CommonScaffoldState>();
|
||||||
|
commonScaffoldState?.actions = [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
globalState.showMessage(
|
||||||
|
title: appLocalizations.reset,
|
||||||
|
message: TextSpan(
|
||||||
|
text: appLocalizations.resetTip,
|
||||||
|
),
|
||||||
|
onTab: () {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
appController.config.vpnProps = defaultVpnProps;
|
||||||
|
appController.clashConfig.tun = defaultTun;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: appLocalizations.reset,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.replay,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_initActions(context);
|
||||||
|
return generateListView(
|
||||||
|
networkItems,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class VPNSwitch extends StatelessWidget {
|
|
||||||
const VPNSwitch({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.vpnProps.enable,
|
|
||||||
builder: (_, enable, __) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
leading: const Icon(Icons.stacked_line_chart),
|
|
||||||
title: const Text("VPN"),
|
|
||||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: enable,
|
|
||||||
onChanged: (bool value) async {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
final vpnProps = config.vpnProps;
|
|
||||||
config.vpnProps = vpnProps.copyWith(
|
|
||||||
enable: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VPNDisabledContainer extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const VPNDisabledContainer(
|
|
||||||
this.child, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.vpnProps.enable,
|
|
||||||
builder: (_, enable, child) {
|
|
||||||
return AbsorbPointer(
|
|
||||||
absorbing: !enable,
|
|
||||||
child: DisabledMask(
|
|
||||||
status: !enable,
|
|
||||||
child: child!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AllowBypassSwitch extends StatelessWidget {
|
|
||||||
const AllowBypassSwitch({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.vpnProps.allowBypass,
|
|
||||||
builder: (_, allowBypass, __) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
leading: const Icon(Icons.arrow_forward_outlined),
|
|
||||||
title: Text(appLocalizations.allowBypass),
|
|
||||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: allowBypass,
|
|
||||||
onChanged: (bool value) async {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
final vpnProps = config.vpnProps;
|
|
||||||
config.vpnProps = vpnProps.copyWith(
|
|
||||||
allowBypass: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SystemProxySwitch extends StatelessWidget {
|
|
||||||
const SystemProxySwitch({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.vpnProps.systemProxy,
|
|
||||||
builder: (_, systemProxy, __) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
leading: const Icon(Icons.settings_ethernet),
|
|
||||||
title: Text(appLocalizations.systemProxy),
|
|
||||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: systemProxy,
|
|
||||||
onChanged: (bool value) async {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
final vpnProps = config.vpnProps;
|
|
||||||
config.vpnProps = vpnProps.copyWith(
|
|
||||||
systemProxy: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VpnOptions extends StatelessWidget {
|
|
||||||
const VpnOptions({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return VPNDisabledContainer(
|
|
||||||
Column(
|
|
||||||
children: generateSection(
|
|
||||||
title: appLocalizations.options,
|
|
||||||
items: [
|
|
||||||
const SystemProxySwitch(),
|
|
||||||
const AllowBypassSwitch(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final vpnItems = [
|
|
||||||
const VPNSwitch(),
|
|
||||||
const VpnOptions(),
|
|
||||||
];
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
|||||||
@@ -13,16 +13,55 @@ class IntranetIP extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _IntranetIPState extends State<IntranetIP> {
|
class _IntranetIPState extends State<IntranetIP> {
|
||||||
final ipNotifier = ValueNotifier<String>("");
|
final ipNotifier = ValueNotifier<String?>("");
|
||||||
|
|
||||||
Future<String?> getLocalIpAddress() async {
|
Future<String> getNetworkType() async {
|
||||||
List<NetworkInterface> interfaces = await NetworkInterface.list();
|
try {
|
||||||
for (final interface in interfaces) {
|
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||||
for (final address in interface.addresses) {
|
includeLoopback: false,
|
||||||
if (!address.isLoopback) {
|
type: InternetAddressType.any,
|
||||||
return address.address;
|
);
|
||||||
|
|
||||||
|
for (var interface in interfaces) {
|
||||||
|
if (interface.name.toLowerCase().contains('wlan') ||
|
||||||
|
interface.name.toLowerCase().contains('wi-fi')) {
|
||||||
|
return 'WiFi';
|
||||||
|
}
|
||||||
|
if (interface.name.toLowerCase().contains('rmnet') ||
|
||||||
|
interface.name.toLowerCase().contains('ccmni') ||
|
||||||
|
interface.name.toLowerCase().contains('cellular')) {
|
||||||
|
return 'Mobile Data';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
|
} catch (e) {
|
||||||
|
return 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLocalIpAddress() async {
|
||||||
|
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||||
|
includeLoopback: false,
|
||||||
|
)
|
||||||
|
..sort((a, b) {
|
||||||
|
if (a.isWifi && !b.isWifi) return -1;
|
||||||
|
if (!a.isWifi && b.isWifi) return 1;
|
||||||
|
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||||
|
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
for (final interface in interfaces) {
|
||||||
|
final addresses = interface.addresses;
|
||||||
|
if (addresses.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addresses.sort((a, b) {
|
||||||
|
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||||
|
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return addresses.first.address;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -48,17 +87,15 @@ class _IntranetIPState extends State<IntranetIP> {
|
|||||||
label: appLocalizations.intranetIP,
|
label: appLocalizations.intranetIP,
|
||||||
iconData: Icons.devices,
|
iconData: Icons.devices,
|
||||||
),
|
),
|
||||||
onPressed: (){
|
onPressed: () {},
|
||||||
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||||
height: globalState.measure.titleLargeHeight + 24 - 2,
|
height: globalState.measure.titleMediumHeight + 24 - 2,
|
||||||
child: ValueListenableBuilder(
|
child: ValueListenableBuilder(
|
||||||
valueListenable: ipNotifier,
|
valueListenable: ipNotifier,
|
||||||
builder: (_, value, __) {
|
builder: (_, value, __) {
|
||||||
return FadeBox(
|
return FadeBox(
|
||||||
child: value.isNotEmpty
|
child: value != null
|
||||||
? Column(
|
? Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -67,8 +104,9 @@ class _IntranetIPState extends State<IntranetIP> {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
child: TooltipText(
|
child: TooltipText(
|
||||||
text: Text(
|
text: Text(
|
||||||
value,
|
value.isNotEmpty ? value : appLocalizations.noNetwork,
|
||||||
style: context.textTheme.titleLarge?.toSoftBold.toMinus,
|
style: context
|
||||||
|
.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
@@ -22,14 +24,17 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
);
|
);
|
||||||
bool? _preIsStart;
|
bool? _preIsStart;
|
||||||
Function? _checkIpDebounce;
|
Function? _checkIpDebounce;
|
||||||
|
Timer? _setTimeoutTimer;
|
||||||
CancelToken? cancelToken;
|
CancelToken? cancelToken;
|
||||||
|
|
||||||
_checkIp() async {
|
_checkIp() async {
|
||||||
final appState = globalState.appController.appState;
|
final appState = globalState.appController.appState;
|
||||||
|
final appFlowingState = globalState.appController.appFlowingState;
|
||||||
final isInit = appState.isInit;
|
final isInit = appState.isInit;
|
||||||
if (!isInit) return;
|
if (!isInit) return;
|
||||||
final isStart = appState.isStart;
|
final isStart = appFlowingState.isStart;
|
||||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||||
|
_clearSetTimeoutTimer();
|
||||||
networkDetectionState.value = networkDetectionState.value.copyWith(
|
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||||
isTesting: true,
|
isTesting: true,
|
||||||
ipInfo: null,
|
ipInfo: null,
|
||||||
@@ -42,11 +47,32 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
cancelToken = CancelToken();
|
cancelToken = CancelToken();
|
||||||
try {
|
try {
|
||||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
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(
|
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||||
isTesting: false,
|
isTesting: true,
|
||||||
ipInfo: ipInfo,
|
ipInfo: null,
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearSetTimeoutTimer() {
|
||||||
|
if(_setTimeoutTimer != null){
|
||||||
|
_setTimeoutTimer?.cancel();
|
||||||
|
_setTimeoutTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkIpContainer(Widget child) {
|
_checkIpContainer(Widget child) {
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
|||||||
label: appLocalizations.networkSpeed,
|
label: appLocalizations.networkSpeed,
|
||||||
iconData: Icons.speed_sharp,
|
iconData: Icons.speed_sharp,
|
||||||
),
|
),
|
||||||
child: Selector<AppState, List<Traffic>>(
|
child: Selector<AppFlowingState, List<Traffic>>(
|
||||||
selector: (_, appState) => appState.traffics,
|
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||||
builder: (_, traffics, __) {
|
builder: (_, traffics, __) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class _StartButtonState extends State<StartButton>
|
|||||||
|
|
||||||
handleSwitchStart() {
|
handleSwitchStart() {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
if (isStart == appController.appState.isStart) {
|
if (isStart == appController.appFlowingState.isStart) {
|
||||||
isStart = !isStart;
|
isStart = !isStart;
|
||||||
updateController();
|
updateController();
|
||||||
appController.updateStatus(isStart);
|
appController.updateStatus(isStart);
|
||||||
@@ -50,8 +50,8 @@ class _StartButtonState extends State<StartButton>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _updateControllerContainer(Widget child) {
|
Widget _updateControllerContainer(Widget child) {
|
||||||
return Selector<AppState, bool>(
|
return Selector<AppFlowingState, bool>(
|
||||||
selector: (_, appState) => appState.isStart,
|
selector: (_, appFlowingState) => appFlowingState.isStart,
|
||||||
builder: (_, isStart, child) {
|
builder: (_, isStart, child) {
|
||||||
if (isStart != this.isStart) {
|
if (isStart != this.isStart) {
|
||||||
this.isStart = isStart;
|
this.isStart = isStart;
|
||||||
@@ -127,8 +127,8 @@ class _StartButtonState extends State<StartButton>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: _updateControllerContainer(
|
child: _updateControllerContainer(
|
||||||
Selector<AppState, int?>(
|
Selector<AppFlowingState, int?>(
|
||||||
selector: (_, appState) => appState.runTime,
|
selector: (_, appFlowingState) => appFlowingState.runTime,
|
||||||
builder: (_, int? value, __) {
|
builder: (_, int? value, __) {
|
||||||
final text = other.getTimeText(value);
|
final text = other.getTimeText(value);
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
@@ -47,14 +47,16 @@ class TUNSwitch extends StatelessWidget {
|
|||||||
child: Selector<ClashConfig, bool>(
|
child: Selector<ClashConfig, bool>(
|
||||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||||
builder: (_, enable, __) {
|
builder: (_, enable, __) {
|
||||||
return Switch(
|
return LocaleBuilder(
|
||||||
value: enable,
|
builder: (_) => Switch(
|
||||||
onChanged: (value) {
|
value: enable,
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChanged: (value) {
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
enable: value,
|
clashConfig.tun = clashConfig.tun.copyWith(
|
||||||
);
|
enable: value,
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -75,14 +77,16 @@ class ProxySwitch extends StatelessWidget {
|
|||||||
child: Selector<Config, bool>(
|
child: Selector<Config, bool>(
|
||||||
selector: (_, config) => config.desktopProps.systemProxy,
|
selector: (_, config) => config.desktopProps.systemProxy,
|
||||||
builder: (_, systemProxy, __) {
|
builder: (_, systemProxy, __) {
|
||||||
return Switch(
|
return LocaleBuilder(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
builder: (_) => Switch(
|
||||||
value: systemProxy,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
onChanged: (value) {
|
value: systemProxy,
|
||||||
final config = globalState.appController.config;
|
onChanged: (value) {
|
||||||
config.desktopProps =
|
final config = globalState.appController.config;
|
||||||
config.desktopProps.copyWith(systemProxy: value);
|
config.desktopProps =
|
||||||
},
|
config.desktopProps.copyWith(systemProxy: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ class TrafficUsage extends StatelessWidget {
|
|||||||
label: appLocalizations.trafficUsage,
|
label: appLocalizations.trafficUsage,
|
||||||
iconData: Icons.data_saver_off,
|
iconData: Icons.data_saver_off,
|
||||||
),
|
),
|
||||||
child: Selector<AppState, Traffic>(
|
child: Selector<AppFlowingState, Traffic>(
|
||||||
selector: (_, appState) => appState.totalTraffic,
|
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
||||||
builder: (_, totalTraffic, __) {
|
builder: (_, totalTraffic, __) {
|
||||||
final upTotalTrafficValue = totalTraffic.up;
|
final upTotalTrafficValue = totalTraffic.up;
|
||||||
final downTotalTrafficValue = totalTraffic.down;
|
final downTotalTrafficValue = totalTraffic.down;
|
||||||
|
|||||||
250
lib/fragments/hotkey.dart
Normal file
250
lib/fragments/hotkey.dart
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/card.dart';
|
||||||
|
import 'package:fl_clash/widgets/list.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
extension IntlExt on Intl {
|
||||||
|
static actionMessage(String messageText) =>
|
||||||
|
Intl.message("action_$messageText");
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotKeyFragment extends StatelessWidget {
|
||||||
|
const HotKeyFragment({super.key});
|
||||||
|
|
||||||
|
String getSubtitle(HotKeyAction hotKeyAction) {
|
||||||
|
final key = hotKeyAction.key;
|
||||||
|
if (key == null) {
|
||||||
|
return appLocalizations.noHotKey;
|
||||||
|
}
|
||||||
|
final modifierLabels =
|
||||||
|
hotKeyAction.modifiers.map((item) => item.physicalKeys.first.label);
|
||||||
|
var text = "";
|
||||||
|
if (modifierLabels.isNotEmpty) {
|
||||||
|
text += "${modifierLabels.join(" ")}+";
|
||||||
|
}
|
||||||
|
text += PhysicalKeyboardKey(key).label;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: HotAction.values.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final hotAction = HotAction.values[index];
|
||||||
|
return Selector<Config, HotKeyAction>(
|
||||||
|
selector: (_, config) {
|
||||||
|
final index = config.hotKeyActions.indexWhere(
|
||||||
|
(item) => item.action == hotAction,
|
||||||
|
);
|
||||||
|
return index != -1
|
||||||
|
? config.hotKeyActions[index]
|
||||||
|
: HotKeyAction(
|
||||||
|
action: hotAction,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
builder: (_, value, __) {
|
||||||
|
return ListItem(
|
||||||
|
title: Text(IntlExt.actionMessage(hotAction.name)),
|
||||||
|
subtitle: Text(
|
||||||
|
getSubtitle(value),
|
||||||
|
style: context.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: context.colorScheme.primary),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
globalState.showCommonDialog(
|
||||||
|
child: HotKeyRecorder(
|
||||||
|
hotKeyAction: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotKeyRecorder extends StatefulWidget {
|
||||||
|
final HotKeyAction hotKeyAction;
|
||||||
|
|
||||||
|
const HotKeyRecorder({
|
||||||
|
super.key,
|
||||||
|
required this.hotKeyAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HotKeyRecorder> createState() => _HotKeyRecorderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||||
|
late ValueNotifier<HotKeyAction> hotKeyActionNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
hotKeyActionNotifier = ValueNotifier<HotKeyAction>(
|
||||||
|
widget.hotKeyAction.copyWith(),
|
||||||
|
);
|
||||||
|
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _handleKeyEvent(KeyEvent keyEvent) {
|
||||||
|
if (keyEvent is KeyUpEvent) return false;
|
||||||
|
final keys = HardwareKeyboard.instance.physicalKeysPressed;
|
||||||
|
|
||||||
|
final key = keyEvent.physicalKey;
|
||||||
|
|
||||||
|
final modifiers = KeyboardModifier.values
|
||||||
|
.where((e) =>
|
||||||
|
e.physicalKeys.any(keys.contains) && !e.physicalKeys.contains(key))
|
||||||
|
.toSet();
|
||||||
|
hotKeyActionNotifier.value = hotKeyActionNotifier.value.copyWith(
|
||||||
|
modifiers: modifiers,
|
||||||
|
key: key.usbHidUsage,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRemove() {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
config.updateOrAddHotKeyAction(
|
||||||
|
hotKeyActionNotifier.value.copyWith(
|
||||||
|
modifiers: {},
|
||||||
|
key: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleConfirm() {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
final currentHotkeyAction = hotKeyActionNotifier.value;
|
||||||
|
if (currentHotkeyAction.key == null ||
|
||||||
|
currentHotkeyAction.modifiers.isEmpty) {
|
||||||
|
globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(text: appLocalizations.inputCorrectHotkey),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hotKeyActions = config.hotKeyActions;
|
||||||
|
final index = hotKeyActions.indexWhere(
|
||||||
|
(item) =>
|
||||||
|
item.key == currentHotkeyAction.key &&
|
||||||
|
keyboardModifierListEquality.equals(
|
||||||
|
item.modifiers,
|
||||||
|
currentHotkeyAction.modifiers,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (index != -1) {
|
||||||
|
globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(text: appLocalizations.hotkeyConflict),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.updateOrAddHotKeyAction(
|
||||||
|
currentHotkeyAction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))),
|
||||||
|
content: ValueListenableBuilder(
|
||||||
|
valueListenable: hotKeyActionNotifier,
|
||||||
|
builder: (_, hotKeyAction, ___) {
|
||||||
|
final key = hotKeyAction.key;
|
||||||
|
final modifiers = hotKeyAction.modifiers;
|
||||||
|
return SizedBox(
|
||||||
|
width: dialogCommonWidth,
|
||||||
|
child: key != null
|
||||||
|
? Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
for (final modifier in modifiers)
|
||||||
|
KeyboardKeyBox(
|
||||||
|
keyboardKey: modifier.physicalKeys.first,
|
||||||
|
),
|
||||||
|
if (modifiers.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
"+",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
KeyboardKeyBox(
|
||||||
|
keyboardKey: PhysicalKeyboardKey(key),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
appLocalizations.pressKeyboard,
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_handleRemove();
|
||||||
|
},
|
||||||
|
child: Text(appLocalizations.remove),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_handleConfirm();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
appLocalizations.confirm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyboardKeyBox extends StatelessWidget {
|
||||||
|
final KeyboardKey keyboardKey;
|
||||||
|
|
||||||
|
const KeyboardKeyBox({
|
||||||
|
super.key,
|
||||||
|
required this.keyboardKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonCard(
|
||||||
|
type: CommonCardType.filled,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
keyboardKey.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
@@ -21,7 +19,6 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
final scrollController = ScrollController(
|
final scrollController = ScrollController(
|
||||||
keepScrollOffset: false,
|
keepScrollOffset: false,
|
||||||
);
|
);
|
||||||
List<GlobalObjectKey<_LogItemState>> keys = [];
|
|
||||||
|
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
|
|
||||||
@@ -29,19 +26,22 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final appState = globalState.appController.appState;
|
final appFlowingState = globalState.appController.appFlowingState;
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(logs: appState.logs);
|
logsNotifier.value =
|
||||||
|
logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||||
final logs = appState.logs;
|
final logs = appFlowingState.logs;
|
||||||
if (!const ListEquality<Log>().equals(
|
if (!logListEquality.equals(
|
||||||
logsNotifier.value.logs,
|
logsNotifier.value.logs,
|
||||||
logs,
|
logs,
|
||||||
)) {
|
)) {
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(logs: logs);
|
logsNotifier.value = logsNotifier.value.copyWith(
|
||||||
|
logs: logs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -56,6 +56,21 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleExport() async {
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||||
|
() async {
|
||||||
|
return await globalState.appController.exportLogs();
|
||||||
|
},
|
||||||
|
title: appLocalizations.exportLogs,
|
||||||
|
);
|
||||||
|
if (res != true) return;
|
||||||
|
globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(text: appLocalizations.exportSuccess),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_initActions() {
|
_initActions() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
final commonScaffoldState =
|
||||||
@@ -72,6 +87,17 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_handleExport();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.file_download_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,15 +138,26 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
child: ValueListenableBuilder<LogsAndKeywords>(
|
child: ValueListenableBuilder<LogsAndKeywords>(
|
||||||
valueListenable: logsNotifier,
|
valueListenable: logsNotifier,
|
||||||
builder: (_, state, __) {
|
builder: (_, state, __) {
|
||||||
var logs = state.filteredLogs;
|
final logs = state.filteredLogs;
|
||||||
if (logs.isEmpty) {
|
if (logs.isEmpty) {
|
||||||
return NullStatus(
|
return NullStatus(
|
||||||
label: appLocalizations.nullLogsDesc,
|
label: appLocalizations.nullLogsDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
logs = logs.reversed.toList();
|
final reversedLogs = logs.reversed.toList();
|
||||||
keys = logs
|
final logWidgets = reversedLogs
|
||||||
.map((log) => GlobalObjectKey<_LogItemState>(log.dateTime))
|
.map<Widget>(
|
||||||
|
(log) => LogItem(
|
||||||
|
key: Key(log.dateTime.toString()),
|
||||||
|
log: log,
|
||||||
|
onClick: _addKeyword,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.separated(
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -147,22 +184,38 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.separated(
|
child: LayoutBuilder(
|
||||||
controller: scrollController,
|
builder: (_, constraints) {
|
||||||
itemBuilder: (_, index) {
|
return ScrollConfiguration(
|
||||||
final log = logs[index];
|
behavior: ShowBarScrollBehavior(),
|
||||||
return LogItem(
|
child: ListView.builder(
|
||||||
key: Key(log.dateTime.toString()),
|
controller: scrollController,
|
||||||
log: log,
|
itemExtentBuilder: (index, __) {
|
||||||
onClick: _addKeyword,
|
final widget = logWidgets[index];
|
||||||
);
|
if (widget.runtimeType == Divider) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final measure = globalState.measure;
|
||||||
|
final bodyLargeSize = measure.bodyLargeSize;
|
||||||
|
final bodySmallHeight = measure.bodySmallHeight;
|
||||||
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||||
|
final log = reversedLogs[(index / 2).floor()];
|
||||||
|
final width = (log.payload?.length ?? 0) *
|
||||||
|
bodyLargeSize.width +
|
||||||
|
200;
|
||||||
|
final lines = (width / constraints.maxWidth).ceil();
|
||||||
|
return lines * bodyLargeSize.height +
|
||||||
|
bodySmallHeight +
|
||||||
|
8 +
|
||||||
|
bodyMediumHeight +
|
||||||
|
40;
|
||||||
|
},
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return logWidgets[index];
|
||||||
|
},
|
||||||
|
itemCount: logWidgets.length,
|
||||||
|
));
|
||||||
},
|
},
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: logs.length,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -236,7 +289,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
|||||||
_addKeyword(String keyword) {
|
_addKeyword(String keyword) {
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||||
if (isContains) return;
|
if (isContains) return;
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)..add(keyword);
|
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||||
|
..add(keyword);
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
logsNotifier.value = logsNotifier.value.copyWith(
|
||||||
keywords: keywords,
|
keywords: keywords,
|
||||||
);
|
);
|
||||||
@@ -245,7 +299,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
|||||||
_deleteKeyword(String keyword) {
|
_deleteKeyword(String keyword) {
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
final isContains = logsNotifier.value.keywords.contains(keyword);
|
||||||
if (!isContains) return;
|
if (!isContains) return;
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)..remove(keyword);
|
final keywords = List<String>.from(logsNotifier.value.keywords)
|
||||||
|
..remove(keyword);
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
logsNotifier.value = logsNotifier.value.copyWith(
|
||||||
keywords: keywords,
|
keywords: keywords,
|
||||||
);
|
);
|
||||||
@@ -330,7 +385,9 @@ class _LogItemState extends State<LogItem> {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
title: SelectableText(log.payload ?? ''),
|
title: SelectableText(
|
||||||
|
log.payload ?? '',
|
||||||
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -339,7 +396,9 @@ class _LogItemState extends State<LogItem> {
|
|||||||
style: context.textTheme.bodySmall
|
style: context.textTheme.bodySmall
|
||||||
?.copyWith(color: context.colorScheme.primary),
|
?.copyWith(color: context.colorScheme.primary),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8,),
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: CommonChip(
|
child: CommonChip(
|
||||||
|
|||||||
@@ -459,10 +459,9 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
child: ReorderableListView.builder(
|
child: ReorderableListView.builder(
|
||||||
buildDefaultDragHandles: false,
|
buildDefaultDragHandles: false,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
proxyDecorator: proxyDecorator,
|
proxyDecorator: proxyDecorator,
|
||||||
onReorder: (int oldIndex, int newIndex) {
|
onReorder: (oldIndex, newIndex) {
|
||||||
if (oldIndex == newIndex) return;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
if (oldIndex < newIndex) {
|
if (oldIndex < newIndex) {
|
||||||
newIndex -= 1;
|
newIndex -= 1;
|
||||||
@@ -497,17 +496,17 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
|||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 8,
|
vertical: 16,
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
child: FilledButton(
|
child: FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
globalState.appController.config.profiles = profiles;
|
globalState.appController.config.profiles = profiles;
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(
|
padding: WidgetStateProperty.all(
|
||||||
const EdgeInsets.symmetric(vertical: 16),
|
const EdgeInsets.symmetric(vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -166,20 +166,20 @@ class ContextMenuControllerImpl implements SelectionToolbarController {
|
|||||||
// _removeOverLayEntry();
|
// _removeOverLayEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleCut(CodeLineEditingController controller) {
|
// _handleCut(CodeLineEditingController controller) {
|
||||||
controller.cut();
|
// controller.cut();
|
||||||
_removeOverLayEntry();
|
// _removeOverLayEntry();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
_handleCopy(CodeLineEditingController controller) async {
|
// _handleCopy(CodeLineEditingController controller) async {
|
||||||
await controller.copy();
|
// await controller.copy();
|
||||||
_removeOverLayEntry();
|
// _removeOverLayEntry();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
_handlePaste(CodeLineEditingController controller) {
|
// _handlePaste(CodeLineEditingController controller) {
|
||||||
controller.paste();
|
// controller.paste();
|
||||||
_removeOverLayEntry();
|
// _removeOverLayEntry();
|
||||||
}
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void show({
|
void show({
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class ProxyCard extends StatelessWidget {
|
|||||||
|
|
||||||
Measure get measure => globalState.measure;
|
Measure get measure => globalState.measure;
|
||||||
|
|
||||||
|
_handleTestCurrentDelay() {
|
||||||
|
proxyDelayTest(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDelayText() {
|
Widget _buildDelayText() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: measure.labelSmallHeight,
|
height: measure.labelSmallHeight,
|
||||||
@@ -36,24 +40,31 @@ class ProxyCard extends StatelessWidget {
|
|||||||
return FadeBox(
|
return FadeBox(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
if (delay == null) {
|
if (delay == 0 || delay == null) {
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
if (delay == 0) {
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: measure.labelSmallHeight,
|
height: measure.labelSmallHeight,
|
||||||
width: measure.labelSmallHeight,
|
width: measure.labelSmallHeight,
|
||||||
child: const CircularProgressIndicator(
|
child: delay == 0
|
||||||
strokeWidth: 2,
|
? const CircularProgressIndicator(
|
||||||
),
|
strokeWidth: 2,
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
icon: const Icon(Icons.bolt),
|
||||||
|
iconSize: globalState.measure.labelSmallHeight,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: _handleTestCurrentDelay,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Text(
|
return GestureDetector(
|
||||||
delay > 0 ? '$delay ms' : "Timeout",
|
onTap: _handleTestCurrentDelay,
|
||||||
style: context.textTheme.labelSmall?.copyWith(
|
child: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
delay > 0 ? '$delay ms' : "Timeout",
|
||||||
color: other.getDelayColor(
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
delay,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
color: other.getDelayColor(
|
||||||
|
delay,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -91,12 +102,12 @@ class ProxyCard extends StatelessWidget {
|
|||||||
|
|
||||||
_changeProxy(BuildContext context) async {
|
_changeProxy(BuildContext context) async {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final isUrlTest = groupType == GroupType.URLTest;
|
final isURLTestOrFallback = groupType.isURLTestOrFallback;
|
||||||
final isSelector = groupType == GroupType.Selector;
|
final isSelector = groupType == GroupType.Selector;
|
||||||
if (isUrlTest || isSelector) {
|
if (isURLTestOrFallback || isSelector) {
|
||||||
final currentProxyName =
|
final currentProxyName =
|
||||||
appController.config.currentSelectedMap[groupName];
|
appController.config.currentSelectedMap[groupName];
|
||||||
final nextProxyName = switch (isUrlTest) {
|
final nextProxyName = switch (isURLTestOrFallback) {
|
||||||
true => currentProxyName == proxy.name ? "" : proxy.name,
|
true => currentProxyName == proxy.name ? "" : proxy.name,
|
||||||
false => proxy.name,
|
false => proxy.name,
|
||||||
};
|
};
|
||||||
@@ -122,7 +133,7 @@ class ProxyCard extends StatelessWidget {
|
|||||||
final measure = globalState.measure;
|
final measure = globalState.measure;
|
||||||
final delayText = _buildDelayText();
|
final delayText = _buildDelayText();
|
||||||
final proxyNameText = _buildProxyNameText(context);
|
final proxyNameText = _buildProxyNameText(context);
|
||||||
return currentGroupProxyNameBuilder(
|
return currentSelectedProxyNameBuilder(
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
builder: (currentGroupName) {
|
builder: (currentGroupName) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -196,30 +207,16 @@ class ProxyCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (groupType == GroupType.URLTest)
|
if (groupType.isURLTestOrFallback)
|
||||||
Selector<Config, String>(
|
Selector<Config, String>(
|
||||||
selector: (_, config) {
|
selector: (_, config) {
|
||||||
final selectedProxyName =
|
final selectedProxyName =
|
||||||
config.currentSelectedMap[groupName];
|
config.currentSelectedMap[groupName];
|
||||||
return selectedProxyName ?? '';
|
return selectedProxyName ?? '';
|
||||||
},
|
},
|
||||||
builder: (_, value, __) {
|
builder: (_, value, child) {
|
||||||
if (value != proxy.name) return Container();
|
if (value != proxy.name) return Container();
|
||||||
return Positioned.fill(
|
return child!;
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
margin: const EdgeInsets.all(8),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
),
|
|
||||||
child: const SelectIcon(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Positioned.fill(
|
child: Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
@@ -8,7 +6,7 @@ import 'package:fl_clash/state.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
Widget currentGroupProxyNameBuilder({
|
Widget currentSelectedProxyNameBuilder({
|
||||||
required String groupName,
|
required String groupName,
|
||||||
required Widget Function(String currentGroupName) builder,
|
required Widget Function(String currentGroupName) builder,
|
||||||
}) {
|
}) {
|
||||||
@@ -18,8 +16,8 @@ Widget currentGroupProxyNameBuilder({
|
|||||||
final selectedProxyName = config.currentSelectedMap[groupName];
|
final selectedProxyName = config.currentSelectedMap[groupName];
|
||||||
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
|
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
|
||||||
},
|
},
|
||||||
builder: (_, currentGroupName, ___) {
|
builder: (_, currentSelectedProxyName, ___) {
|
||||||
return builder(currentGroupName);
|
return builder(currentSelectedProxyName);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,10 +38,26 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyDelayTest(Proxy proxy) async {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||||
|
globalState.appController.setDelay(
|
||||||
|
Delay(
|
||||||
|
name: proxyName,
|
||||||
|
value: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
globalState.appController.setDelay(await clashCore.getDelay(proxyName));
|
||||||
|
}
|
||||||
|
|
||||||
delayTest(List<Proxy> proxies) async {
|
delayTest(List<Proxy> proxies) async {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final delayProxies = proxies.map<Future>((proxy) async {
|
final proxyNames = proxies
|
||||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||||
globalState.appController.setDelay(
|
globalState.appController.setDelay(
|
||||||
Delay(
|
Delay(
|
||||||
name: proxyName,
|
name: proxyName,
|
||||||
@@ -67,14 +81,14 @@ double getScrollToSelectedOffset({
|
|||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final columns = other.getProxiesColumns(
|
final columns = other.getProxiesColumns(
|
||||||
appController.appState.viewWidth,
|
appController.appState.viewWidth,
|
||||||
appController.config.proxiesLayout,
|
appController.config.proxiesStyle.layout,
|
||||||
);
|
);
|
||||||
final proxyCardType = appController.config.proxyCardType;
|
final proxyCardType = appController.config.proxiesStyle.cardType;
|
||||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
final selectedName = appController.getCurrentSelectedName(groupName);
|
||||||
final findSelectedIndex = proxies.indexWhere(
|
final findSelectedIndex = proxies.indexWhere(
|
||||||
(proxy) => proxy.name == selectedName,
|
(proxy) => proxy.name == selectedName,
|
||||||
);
|
);
|
||||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||||
final rows = (selectedIndex / columns).floor();
|
final rows = (selectedIndex / columns).floor();
|
||||||
return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0);
|
return rows * getItemHeight(proxyCardType) + (rows - 1) * 8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'dart:math';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/card.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:fl_clash/widgets/fade_box.dart';
|
|
||||||
import 'package:fl_clash/widgets/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -219,11 +217,15 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
final currentInitOffset = _headerOffset[index];
|
final currentInitOffset = _headerOffset[index];
|
||||||
final proxies = _lastGroupNameProxiesMap[groupName];
|
final proxies = _lastGroupNameProxiesMap[groupName];
|
||||||
_controller.animateTo(
|
_controller.animateTo(
|
||||||
currentInitOffset +
|
min(
|
||||||
getScrollToSelectedOffset(
|
currentInitOffset +
|
||||||
groupName: groupName,
|
8 +
|
||||||
proxies: proxies ?? [],
|
getScrollToSelectedOffset(
|
||||||
),
|
groupName: groupName,
|
||||||
|
proxies: proxies ?? [],
|
||||||
|
),
|
||||||
|
_controller.position.maxScrollExtent,
|
||||||
|
),
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeIn,
|
curve: Curves.easeIn,
|
||||||
);
|
);
|
||||||
@@ -238,18 +240,17 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
return ProxiesListSelectorState(
|
return ProxiesListSelectorState(
|
||||||
groupNames: groupNames,
|
groupNames: groupNames,
|
||||||
currentUnfoldSet: config.currentUnfoldSet,
|
currentUnfoldSet: config.currentUnfoldSet,
|
||||||
proxyCardType: config.proxyCardType,
|
proxyCardType: config.proxiesStyle.cardType,
|
||||||
proxiesSortType: config.proxiesSortType,
|
proxiesSortType: config.proxiesStyle.sortType,
|
||||||
columns: other.getProxiesColumns(
|
columns: other.getProxiesColumns(
|
||||||
appState.viewWidth,
|
appState.viewWidth,
|
||||||
config.proxiesLayout,
|
config.proxiesStyle.layout,
|
||||||
),
|
),
|
||||||
sortNum: appState.sortNum,
|
sortNum: appState.sortNum,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
shouldRebuild: (prev, next) {
|
shouldRebuild: (prev, next) {
|
||||||
if (!const ListEquality<String>()
|
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
|
||||||
.equals(prev.groupNames, next.groupNames)) {
|
|
||||||
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
|
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
|
||||||
offset: 0,
|
offset: 0,
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
@@ -371,11 +372,6 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleChange(String groupName) {
|
_handleChange(String groupName) {
|
||||||
if (isExpand) {
|
|
||||||
_animationController.reverse();
|
|
||||||
} else {
|
|
||||||
_animationController.forward();
|
|
||||||
}
|
|
||||||
widget.onChange(groupName);
|
widget.onChange(groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,13 +401,69 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.isExpand != widget.isExpand) {
|
if (oldWidget.isExpand != widget.isExpand) {
|
||||||
if (isExpand) {
|
if (isExpand) {
|
||||||
_animationController.value = 1.0;
|
_animationController.forward();
|
||||||
} else {
|
} else {
|
||||||
_animationController.value = 0.0;
|
_animationController.reverse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildIcon() {
|
||||||
|
return Selector<Config, ProxiesIconStyle>(
|
||||||
|
selector: (_, config) => config.proxiesStyle.iconStyle,
|
||||||
|
builder: (_, iconStyle, child) {
|
||||||
|
return Selector<Config, String>(
|
||||||
|
selector: (_, config) {
|
||||||
|
final iconMapEntryList =
|
||||||
|
config.proxiesStyle.iconMap.entries.toList();
|
||||||
|
final index = iconMapEntryList.indexWhere((item) {
|
||||||
|
try{
|
||||||
|
return RegExp(item.key).hasMatch(groupName);
|
||||||
|
}catch(_){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (index != -1) {
|
||||||
|
return iconMapEntryList[index].value;
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
},
|
||||||
|
builder: (_, icon, __) {
|
||||||
|
return switch (iconStyle) {
|
||||||
|
ProxiesIconStyle.standard => Container(
|
||||||
|
height: 48,
|
||||||
|
width: 48,
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: CommonIcon(
|
||||||
|
src: icon,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ProxiesIconStyle.icon => Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
child: CommonIcon(
|
||||||
|
src: icon,
|
||||||
|
size: 42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ProxiesIconStyle.none => Container(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CommonCard(
|
return CommonCard(
|
||||||
@@ -419,41 +471,17 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
radius: 24,
|
radius: 24,
|
||||||
type: CommonCardType.filled,
|
type: CommonCardType.filled,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
_buildIcon(),
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 48,
|
|
||||||
width: 48,
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.colorScheme.secondaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: icon.isNotEmpty
|
|
||||||
? CachedNetworkImage(
|
|
||||||
imageUrl: icon,
|
|
||||||
errorWidget: (_, __, ___) => const Icon(
|
|
||||||
IconsExt.target,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(
|
|
||||||
IconsExt.target,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -479,7 +507,7 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: currentGroupProxyNameBuilder(
|
child: currentSelectedProxyNameBuilder(
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
builder: (currentGroupName) {
|
builder: (currentGroupName) {
|
||||||
return Row(
|
return Row(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:fl_clash/common/app_localizations.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/fragments/proxies/list.dart';
|
import 'package:fl_clash/fragments/proxies/list.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'providers.dart';
|
import 'providers.dart';
|
||||||
import 'setting.dart';
|
import 'setting.dart';
|
||||||
import 'tab.dart';
|
import 'tab.dart';
|
||||||
@@ -37,7 +37,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.swap_vert_circle_outlined,
|
Icons.poll_outlined,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -56,6 +56,63 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
)
|
)
|
||||||
|
] else ...[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showExtendPage(
|
||||||
|
context,
|
||||||
|
extendPageWidth: 360,
|
||||||
|
title: appLocalizations.iconConfiguration,
|
||||||
|
body: Selector<Config, Map<String, String>>(
|
||||||
|
selector: (_, config) => config.proxiesStyle.iconMap,
|
||||||
|
shouldRebuild: (prev, next) {
|
||||||
|
return !stringAndStringMapEntryIterableEquality.equals(
|
||||||
|
prev.entries,
|
||||||
|
next.entries,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
builder: (_, iconMap, __) {
|
||||||
|
final entries = iconMap.entries.toList();
|
||||||
|
return ListPage(
|
||||||
|
title: appLocalizations.iconConfiguration,
|
||||||
|
items: entries,
|
||||||
|
keyLabel: appLocalizations.regExp,
|
||||||
|
valueLabel: appLocalizations.icon,
|
||||||
|
keyBuilder: (item) => Key(item.key),
|
||||||
|
titleBuilder: (item) => Text(item.key),
|
||||||
|
leadingBuilder: (item) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: CommonIcon(
|
||||||
|
src: item.value,
|
||||||
|
size: 42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitleBuilder: (item) => Text(
|
||||||
|
item.value,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onChange: (entries) {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||||
|
iconMap: Map.fromEntries(entries),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.style_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -63,7 +120,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
|||||||
title: appLocalizations.proxiesSetting,
|
title: appLocalizations.proxiesSetting,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return const ProxiesSettingWidget();
|
return const ProxiesSetting();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -78,7 +135,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Config, ProxiesType>(
|
return Selector<Config, ProxiesType>(
|
||||||
selector: (_, config) => config.proxiesType,
|
selector: (_, config) => config.proxiesStyle.type,
|
||||||
builder: (_, proxiesType, __) {
|
builder: (_, proxiesType, __) {
|
||||||
return ProxiesActionsBuilder(
|
return ProxiesActionsBuilder(
|
||||||
builder: (state, child) {
|
builder: (state, child) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ProxiesSettingWidget extends StatelessWidget {
|
class ProxiesSetting extends StatelessWidget {
|
||||||
const ProxiesSettingWidget({super.key});
|
const ProxiesSetting({super.key});
|
||||||
|
|
||||||
IconData _getIconWithProxiesType(ProxiesType type) {
|
IconData _getIconWithProxiesType(ProxiesType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
@@ -41,6 +41,14 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getTextWithProxiesIconStyle(ProxiesIconStyle style) {
|
||||||
|
return switch (style) {
|
||||||
|
ProxiesIconStyle.standard => appLocalizations.standard,
|
||||||
|
ProxiesIconStyle.none => appLocalizations.noIcon,
|
||||||
|
ProxiesIconStyle.icon => appLocalizations.onlyIcon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _buildStyleSetting() {
|
List<Widget> _buildStyleSetting() {
|
||||||
return generateSection(
|
return generateSection(
|
||||||
title: appLocalizations.style,
|
title: appLocalizations.style,
|
||||||
@@ -49,7 +57,7 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, ProxiesType>(
|
child: Selector<Config, ProxiesType>(
|
||||||
selector: (_, config) => config.proxiesType,
|
selector: (_, config) => config.proxiesStyle.type,
|
||||||
builder: (_, proxiesType, __) {
|
builder: (_, proxiesType, __) {
|
||||||
final config = globalState.appController.config;
|
final config = globalState.appController.config;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
@@ -63,7 +71,9 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isSelected: proxiesType == item,
|
isSelected: proxiesType == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
config.proxiesType = item;
|
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||||
|
type: item,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -83,7 +93,7 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, ProxiesSortType>(
|
child: Selector<Config, ProxiesSortType>(
|
||||||
selector: (_, config) => config.proxiesSortType,
|
selector: (_, config) => config.proxiesStyle.sortType,
|
||||||
builder: (_, proxiesSortType, __) {
|
builder: (_, proxiesSortType, __) {
|
||||||
final config = globalState.appController.config;
|
final config = globalState.appController.config;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
@@ -97,7 +107,9 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isSelected: proxiesSortType == item,
|
isSelected: proxiesSortType == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
config.proxiesSortType = item;
|
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||||
|
sortType: item,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -117,7 +129,7 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, ProxyCardType>(
|
child: Selector<Config, ProxyCardType>(
|
||||||
selector: (_, config) => config.proxyCardType,
|
selector: (_, config) => config.proxiesStyle.cardType,
|
||||||
builder: (_, proxyCardType, __) {
|
builder: (_, proxyCardType, __) {
|
||||||
final config = globalState.appController.config;
|
final config = globalState.appController.config;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
@@ -128,7 +140,9 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
Intl.message(item.name),
|
Intl.message(item.name),
|
||||||
isSelected: item == proxyCardType,
|
isSelected: item == proxyCardType,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
config.proxyCardType = item;
|
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||||
|
cardType: item,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -149,8 +163,8 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector< Config, ProxiesLayout>(
|
child: Selector<Config, ProxiesLayout>(
|
||||||
selector: (_, config) => config.proxiesLayout,
|
selector: (_, config) => config.proxiesStyle.layout,
|
||||||
builder: (_, proxiesLayout, __) {
|
builder: (_, proxiesLayout, __) {
|
||||||
final config = globalState.appController.config;
|
final config = globalState.appController.config;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
@@ -161,7 +175,9 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
getTextForProxiesLayout(item),
|
getTextForProxiesLayout(item),
|
||||||
isSelected: item == proxiesLayout,
|
isSelected: item == proxiesLayout,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
config.proxiesLayout = item;
|
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||||
|
layout: item,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -173,6 +189,39 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildGroupStyleSetting() {
|
||||||
|
return generateSection(
|
||||||
|
title: "图标样式",
|
||||||
|
items: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Selector<Config, ProxiesIconStyle>(
|
||||||
|
selector: (_, config) => config.proxiesStyle.iconStyle,
|
||||||
|
builder: (_, iconStyle, __) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
for (final item in ProxiesIconStyle.values)
|
||||||
|
SettingTextCard(
|
||||||
|
_getTextWithProxiesIconStyle(item),
|
||||||
|
isSelected: iconStyle == item,
|
||||||
|
onPressed: () {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||||
|
iconStyle: item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -185,6 +234,22 @@ class ProxiesSettingWidget extends StatelessWidget {
|
|||||||
..._buildSortSetting(),
|
..._buildSortSetting(),
|
||||||
..._buildLayoutSetting(),
|
..._buildLayoutSetting(),
|
||||||
..._buildSizeSetting(),
|
..._buildSizeSetting(),
|
||||||
|
Selector<Config, bool>(
|
||||||
|
selector: (_, config) =>
|
||||||
|
config.proxiesStyle.type == ProxiesType.list,
|
||||||
|
builder: (_, value, child) {
|
||||||
|
if (value) {
|
||||||
|
return child!;
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
..._buildGroupStyleSetting(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'dart:math';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
@@ -118,8 +118,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
shouldRebuild: (prev, next) {
|
shouldRebuild: (prev, next) {
|
||||||
if (!const ListEquality<String>()
|
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
|
||||||
.equals(prev.groupNames, next.groupNames)) {
|
|
||||||
_tabController?.dispose();
|
_tabController?.dispose();
|
||||||
_tabController = null;
|
_tabController = null;
|
||||||
return true;
|
return true;
|
||||||
@@ -138,7 +137,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
GroupNameKeyMap keyMap = {};
|
GroupNameKeyMap keyMap = {};
|
||||||
final children = state.groupNames.map((groupName) {
|
final children = state.groupNames.map((groupName) {
|
||||||
keyMap[groupName] = GlobalObjectKey(groupName);
|
keyMap[groupName] = GlobalObjectKey(groupName);
|
||||||
return KeepContainer(
|
return KeepScope(
|
||||||
child: ProxyGroupView(
|
child: ProxyGroupView(
|
||||||
key: keyMap[groupName],
|
key: keyMap[groupName],
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
@@ -266,11 +265,14 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_controller.animateTo(
|
_controller.animateTo(
|
||||||
16 +
|
min(
|
||||||
getScrollToSelectedOffset(
|
16 +
|
||||||
groupName: groupName,
|
getScrollToSelectedOffset(
|
||||||
proxies: _lastProxies,
|
groupName: groupName,
|
||||||
),
|
proxies: _lastProxies,
|
||||||
|
),
|
||||||
|
_controller.position.maxScrollExtent,
|
||||||
|
),
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeIn,
|
curve: Curves.easeIn,
|
||||||
);
|
);
|
||||||
@@ -282,11 +284,11 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
|||||||
selector: (_, appState, config) {
|
selector: (_, appState, config) {
|
||||||
final group = appState.getGroupWithName(groupName)!;
|
final group = appState.getGroupWithName(groupName)!;
|
||||||
return ProxyGroupSelectorState(
|
return ProxyGroupSelectorState(
|
||||||
proxyCardType: config.proxyCardType,
|
proxyCardType: config.proxiesStyle.cardType,
|
||||||
proxiesSortType: config.proxiesSortType,
|
proxiesSortType: config.proxiesStyle.sortType,
|
||||||
columns: other.getProxiesColumns(
|
columns: other.getProxiesColumns(
|
||||||
appState.viewWidth,
|
appState.viewWidth,
|
||||||
config.proxiesLayout,
|
config.proxiesStyle.layout,
|
||||||
),
|
),
|
||||||
sortNum: appState.sortNum,
|
sortNum: appState.sortNum,
|
||||||
proxies: group.all,
|
proxies: group.all,
|
||||||
@@ -311,7 +313,12 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
|||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 96,
|
||||||
|
),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: columns,
|
crossAxisCount: columns,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
@@ -37,8 +36,11 @@ class _RequestsFragmentState extends State<RequestsFragment> {
|
|||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||||
final requests = appState.requests;
|
final maxLength = Platform.isAndroid ? 1000 : 60;
|
||||||
if (!const ListEquality<Connection>().equals(
|
final requests = appState.requests.safeSublist(
|
||||||
|
appState.requests.length - maxLength,
|
||||||
|
);
|
||||||
|
if (!connectionListEquality.equals(
|
||||||
requestsNotifier.value.connections,
|
requestsNotifier.value.connections,
|
||||||
requests,
|
requests,
|
||||||
)) {
|
)) {
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import 'package:fl_clash/fragments/about.dart';
|
|||||||
import 'package:fl_clash/fragments/access.dart';
|
import 'package:fl_clash/fragments/access.dart';
|
||||||
import 'package:fl_clash/fragments/application_setting.dart';
|
import 'package:fl_clash/fragments/application_setting.dart';
|
||||||
import 'package:fl_clash/fragments/config/config.dart';
|
import 'package:fl_clash/fragments/config/config.dart';
|
||||||
|
import 'package:fl_clash/fragments/hotkey.dart';
|
||||||
import 'package:fl_clash/l10n/l10n.dart';
|
import 'package:fl_clash/l10n/l10n.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../widgets/widgets.dart';
|
|
||||||
import 'backup_and_recovery.dart';
|
import 'backup_and_recovery.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
import 'package:path/path.dart' show dirname, join;
|
||||||
|
|
||||||
class ToolsFragment extends StatefulWidget {
|
class ToolsFragment extends StatefulWidget {
|
||||||
const ToolsFragment({super.key});
|
const ToolsFragment({super.key});
|
||||||
@@ -61,6 +63,17 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
return generateSection(
|
return generateSection(
|
||||||
title: appLocalizations.other,
|
title: appLocalizations.other,
|
||||||
items: [
|
items: [
|
||||||
|
ListItem(
|
||||||
|
leading: const Icon(Icons.gavel),
|
||||||
|
title: Text(appLocalizations.disclaimer),
|
||||||
|
onTap: () async {
|
||||||
|
final isDisclaimerAccepted =
|
||||||
|
await globalState.appController.showDisclaimer();
|
||||||
|
if (!isDisclaimerAccepted) {
|
||||||
|
globalState.appController.handleExit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
ListItem.open(
|
ListItem.open(
|
||||||
leading: const Icon(Icons.info),
|
leading: const Icon(Icons.info),
|
||||||
title: Text(appLocalizations.about),
|
title: Text(appLocalizations.about),
|
||||||
@@ -78,7 +91,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
title: appLocalizations.settings,
|
title: appLocalizations.settings,
|
||||||
items: [
|
items: [
|
||||||
Selector<Config, String?>(
|
Selector<Config, String?>(
|
||||||
selector: (_, config) => config.locale,
|
selector: (_, config) => config.appSetting.locale,
|
||||||
builder: (_, localeString, __) {
|
builder: (_, localeString, __) {
|
||||||
final subTitle = localeString ?? appLocalizations.defaultText;
|
final subTitle = localeString ?? appLocalizations.defaultText;
|
||||||
final currentLocale = other.getLocaleForString(localeString);
|
final currentLocale = other.getLocaleForString(localeString);
|
||||||
@@ -88,13 +101,12 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
subtitle: Text(Intl.message(subTitle)),
|
subtitle: Text(Intl.message(subTitle)),
|
||||||
delegate: OptionsDelegate(
|
delegate: OptionsDelegate(
|
||||||
title: appLocalizations.language,
|
title: appLocalizations.language,
|
||||||
options: [
|
options: [null, ...AppLocalizations.delegate.supportedLocales],
|
||||||
null,
|
|
||||||
...AppLocalizations.delegate.supportedLocales
|
|
||||||
],
|
|
||||||
onChanged: (Locale? value) {
|
onChanged: (Locale? value) {
|
||||||
final config = context.read<Config>();
|
final config = globalState.appController.config;
|
||||||
config.locale = value?.toString();
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
locale: value?.toString(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
textBuilder: (locale) => _getLocaleString(locale),
|
textBuilder: (locale) => _getLocaleString(locale),
|
||||||
value: currentLocale,
|
value: currentLocale,
|
||||||
@@ -121,6 +133,28 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
widget: const BackupAndRecovery(),
|
widget: const BackupAndRecovery(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (system.isDesktop)
|
||||||
|
ListItem.open(
|
||||||
|
leading: const Icon(Icons.keyboard),
|
||||||
|
title: Text(appLocalizations.hotkeyManagement),
|
||||||
|
subtitle: Text(appLocalizations.hotkeyManagementDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.hotkeyManagement,
|
||||||
|
widget: const HotKeyFragment(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (Platform.isWindows)
|
||||||
|
ListItem(
|
||||||
|
leading: const Icon(Icons.lock),
|
||||||
|
title: Text(appLocalizations.loopback),
|
||||||
|
subtitle: Text(appLocalizations.loopbackDesc),
|
||||||
|
onTap: () {
|
||||||
|
windows?.runas(
|
||||||
|
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
ListItem.open(
|
ListItem.open(
|
||||||
leading: const Icon(Icons.view_list),
|
leading: const Icon(Icons.view_list),
|
||||||
@@ -155,9 +189,8 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Config, String?>(
|
return LocaleBuilder(
|
||||||
selector: (_, config) => config.locale,
|
builder: (_) {
|
||||||
builder: (_, __, ___) {
|
|
||||||
final items = [
|
final items = [
|
||||||
Selector<AppState, MoreToolsSelectorState>(
|
Selector<AppState, MoreToolsSelectorState>(
|
||||||
selector: (_, appState) {
|
selector: (_, appState) {
|
||||||
@@ -190,6 +223,7 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (_, index) => items[index],
|
itemBuilder: (_, index) => items[index],
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"tunDesc": "only effective in administrator mode",
|
"tunDesc": "only effective in administrator mode",
|
||||||
"minimizeOnExit": "Minimize on exit",
|
"minimizeOnExit": "Minimize on exit",
|
||||||
"minimizeOnExitDesc": "Modify the default system exit event",
|
"minimizeOnExitDesc": "Modify the default system exit event",
|
||||||
"autoLaunch": "AutoLaunch",
|
"autoLaunch": "Auto launch",
|
||||||
"autoLaunchDesc": "Follow the system self startup",
|
"autoLaunchDesc": "Follow the system self startup",
|
||||||
"silentLaunch": "SilentLaunch",
|
"silentLaunch": "SilentLaunch",
|
||||||
"silentLaunchDesc": "Start in the background",
|
"silentLaunchDesc": "Start in the background",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
"tabAnimation": "Tab animation",
|
"tabAnimation": "Tab animation",
|
||||||
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
|
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
|
||||||
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
|
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
|
||||||
"startVpn": "Staring VPN...",
|
"startVpn": "Starting VPN...",
|
||||||
"stopVpn": "Stopping VPN...",
|
"stopVpn": "Stopping VPN...",
|
||||||
"discovery": "Discovery a new version",
|
"discovery": "Discovery a new version",
|
||||||
"compatible": "Compatibility mode",
|
"compatible": "Compatibility mode",
|
||||||
@@ -250,8 +250,7 @@
|
|||||||
"dnsDesc": "Update DNS related settings",
|
"dnsDesc": "Update DNS related settings",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"keyNotEmpty": "The key cannot be empty",
|
"notEmpty": "Cannot be empty",
|
||||||
"valueNotEmpty": "The value cannot be empty",
|
|
||||||
"hostsDesc": "Add Hosts",
|
"hostsDesc": "Add Hosts",
|
||||||
"vpnTip": "Changes take effect after restarting the VPN",
|
"vpnTip": "Changes take effect after restarting the VPN",
|
||||||
"vpnEnableDesc": "Auto routes all system traffic through VpnService",
|
"vpnEnableDesc": "Auto routes all system traffic through VpnService",
|
||||||
@@ -287,6 +286,39 @@
|
|||||||
"geoipCode": "Geoip code",
|
"geoipCode": "Geoip code",
|
||||||
"ipcidr": "Ipcidr",
|
"ipcidr": "Ipcidr",
|
||||||
"domain": "Domain",
|
"domain": "Domain",
|
||||||
"resetDns": "Reset Dns",
|
"reset": "Reset",
|
||||||
"reset": "Reset"
|
"action_view": "Show/Hide",
|
||||||
|
"action_start": "Start/Stop",
|
||||||
|
"action_mode": "Switch mode",
|
||||||
|
"action_proxy": "System proxy",
|
||||||
|
"action_tun": "TUN",
|
||||||
|
"disclaimer": "Disclaimer",
|
||||||
|
"disclaimerDesc": "This software is only used for non-commercial purposes such as learning exchanges and scientific research. It is strictly prohibited to use this software for commercial purposes. Any commercial activity, if any, has nothing to do with this software.",
|
||||||
|
"agree": "Agree",
|
||||||
|
"hotkeyManagement": "Hotkey Management",
|
||||||
|
"hotkeyManagementDesc": "Use keyboard to control applications",
|
||||||
|
"pressKeyboard": "Please press the keyboard.",
|
||||||
|
"inputCorrectHotkey": "Please enter the correct hotkey",
|
||||||
|
"hotkeyConflict": "Hotkey conflict",
|
||||||
|
"remove": "Remove",
|
||||||
|
"noHotKey": "No HotKey",
|
||||||
|
"noNetwork": "No network",
|
||||||
|
"ipv6InboundDesc": "Allow IPv6 inbound",
|
||||||
|
"exportLogs": "Export logs",
|
||||||
|
"exportSuccess": "Export Success",
|
||||||
|
"iconStyle": "Icon style",
|
||||||
|
"onlyIcon": "Icon",
|
||||||
|
"noIcon": "None",
|
||||||
|
"stackMode": "Stack mode",
|
||||||
|
"network": "Network",
|
||||||
|
"networkDesc": "Modify network-related settings",
|
||||||
|
"bypassDomain": "Bypass domain",
|
||||||
|
"bypassDomainDesc": "Only takes effect when the system proxy is enabled",
|
||||||
|
"resetTip": "Make sure to reset",
|
||||||
|
"regExp": "RegExp",
|
||||||
|
"icon": "Icon",
|
||||||
|
"iconConfiguration": "Icon configuration",
|
||||||
|
"noData": "No data",
|
||||||
|
"adminAutoLaunch": "Admin auto launch",
|
||||||
|
"adminAutoLaunchDesc": "Boot up by using admin mode"
|
||||||
}
|
}
|
||||||
@@ -238,9 +238,9 @@
|
|||||||
"clipboardImport": "剪贴板导入",
|
"clipboardImport": "剪贴板导入",
|
||||||
"clipboardExport": "导出剪贴板",
|
"clipboardExport": "导出剪贴板",
|
||||||
"layout": "布局",
|
"layout": "布局",
|
||||||
"tight": "宽松",
|
"tight": "紧凑",
|
||||||
"standard": "标准",
|
"standard": "标准",
|
||||||
"loose": "紧凑",
|
"loose": "宽松",
|
||||||
"profilesSort": "配置排序",
|
"profilesSort": "配置排序",
|
||||||
"start": "启动",
|
"start": "启动",
|
||||||
"stop": "暂停",
|
"stop": "暂停",
|
||||||
@@ -250,8 +250,7 @@
|
|||||||
"dnsDesc": "更新DNS相关设置",
|
"dnsDesc": "更新DNS相关设置",
|
||||||
"key": "键",
|
"key": "键",
|
||||||
"value": "值",
|
"value": "值",
|
||||||
"keyNotEmpty": "键不能为空",
|
"notEmpty": "不能为空",
|
||||||
"valueNotEmpty": "值不能为空",
|
|
||||||
"hostsDesc": "追加Hosts",
|
"hostsDesc": "追加Hosts",
|
||||||
"vpnTip": "重启VPN后改变生效",
|
"vpnTip": "重启VPN后改变生效",
|
||||||
"vpnEnableDesc": "通过VpnService自动路由系统所有流量",
|
"vpnEnableDesc": "通过VpnService自动路由系统所有流量",
|
||||||
@@ -287,6 +286,39 @@
|
|||||||
"geoipCode": "Geoip代码",
|
"geoipCode": "Geoip代码",
|
||||||
"ipcidr": "IP/掩码",
|
"ipcidr": "IP/掩码",
|
||||||
"domain": "域名",
|
"domain": "域名",
|
||||||
"resetDns": "重置DNS",
|
"reset": "重置",
|
||||||
"reset": "重置"
|
"action_view": "显示/隐藏",
|
||||||
|
"action_start": "启动/停止",
|
||||||
|
"action_mode": "切换模式",
|
||||||
|
"action_proxy": "系统代理",
|
||||||
|
"action_tun": "虚拟网卡",
|
||||||
|
"disclaimer": "免责声明",
|
||||||
|
"disclaimerDesc": "本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
|
||||||
|
"agree": "同意",
|
||||||
|
"hotkeyManagement": "快捷键管理",
|
||||||
|
"hotkeyManagementDesc": "使用键盘控制应用程序",
|
||||||
|
"pressKeyboard": "请按下按键",
|
||||||
|
"inputCorrectHotkey": "请输入正确的快捷键",
|
||||||
|
"hotkeyConflict": "快捷键冲突",
|
||||||
|
"remove": "移除",
|
||||||
|
"noHotKey": "暂无快捷键",
|
||||||
|
"noNetwork": "无网络",
|
||||||
|
"ipv6InboundDesc": "允许IPv6入站",
|
||||||
|
"exportLogs": "导出日志",
|
||||||
|
"exportSuccess": "导出成功",
|
||||||
|
"iconStyle": "图标样式",
|
||||||
|
"onlyIcon": "仅图标",
|
||||||
|
"noIcon": "无图标",
|
||||||
|
"stackMode": "栈模式",
|
||||||
|
"network": "网络",
|
||||||
|
"networkDesc": "修改网络相关设置",
|
||||||
|
"bypassDomain": "排除域名",
|
||||||
|
"bypassDomainDesc": "仅在系统代理启用时生效",
|
||||||
|
"resetTip": "确定要重置吗?",
|
||||||
|
"regExp": "正则",
|
||||||
|
"icon": "图片",
|
||||||
|
"iconConfiguration": "图片配置",
|
||||||
|
"noData": "暂无数据",
|
||||||
|
"adminAutoLaunch": "管理员自启动",
|
||||||
|
"adminAutoLaunchDesc": "使用管理员模式开机自启动"
|
||||||
}
|
}
|
||||||
@@ -34,13 +34,23 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"accountTip":
|
"accountTip":
|
||||||
MessageLookupByLibrary.simpleMessage("Account cannot be empty"),
|
MessageLookupByLibrary.simpleMessage("Account cannot be empty"),
|
||||||
"action": MessageLookupByLibrary.simpleMessage("Action"),
|
"action": MessageLookupByLibrary.simpleMessage("Action"),
|
||||||
|
"action_mode": MessageLookupByLibrary.simpleMessage("Switch mode"),
|
||||||
|
"action_proxy": MessageLookupByLibrary.simpleMessage("System proxy"),
|
||||||
|
"action_start": MessageLookupByLibrary.simpleMessage("Start/Stop"),
|
||||||
|
"action_tun": MessageLookupByLibrary.simpleMessage("TUN"),
|
||||||
|
"action_view": MessageLookupByLibrary.simpleMessage("Show/Hide"),
|
||||||
"add": MessageLookupByLibrary.simpleMessage("Add"),
|
"add": MessageLookupByLibrary.simpleMessage("Add"),
|
||||||
"address": MessageLookupByLibrary.simpleMessage("Address"),
|
"address": MessageLookupByLibrary.simpleMessage("Address"),
|
||||||
"addressHelp":
|
"addressHelp":
|
||||||
MessageLookupByLibrary.simpleMessage("WebDAV server address"),
|
MessageLookupByLibrary.simpleMessage("WebDAV server address"),
|
||||||
"addressTip": MessageLookupByLibrary.simpleMessage(
|
"addressTip": MessageLookupByLibrary.simpleMessage(
|
||||||
"Please enter a valid WebDAV address"),
|
"Please enter a valid WebDAV address"),
|
||||||
|
"adminAutoLaunch":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Admin auto launch"),
|
||||||
|
"adminAutoLaunchDesc":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Boot up by using admin mode"),
|
||||||
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
|
"ago": MessageLookupByLibrary.simpleMessage(" Ago"),
|
||||||
|
"agree": MessageLookupByLibrary.simpleMessage("Agree"),
|
||||||
"allApps": MessageLookupByLibrary.simpleMessage("All apps"),
|
"allApps": MessageLookupByLibrary.simpleMessage("All apps"),
|
||||||
"allowBypass": MessageLookupByLibrary.simpleMessage(
|
"allowBypass": MessageLookupByLibrary.simpleMessage(
|
||||||
"Allow applications to bypass VPN"),
|
"Allow applications to bypass VPN"),
|
||||||
@@ -66,7 +76,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("Auto lose connections"),
|
MessageLookupByLibrary.simpleMessage("Auto lose connections"),
|
||||||
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
|
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Auto close connections after change node"),
|
"Auto close connections after change node"),
|
||||||
"autoLaunch": MessageLookupByLibrary.simpleMessage("AutoLaunch"),
|
"autoLaunch": MessageLookupByLibrary.simpleMessage("Auto launch"),
|
||||||
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage(
|
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Follow the system self startup"),
|
"Follow the system self startup"),
|
||||||
"autoRun": MessageLookupByLibrary.simpleMessage("AutoRun"),
|
"autoRun": MessageLookupByLibrary.simpleMessage("AutoRun"),
|
||||||
@@ -83,6 +93,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"),
|
"backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"),
|
||||||
"bind": MessageLookupByLibrary.simpleMessage("Bind"),
|
"bind": MessageLookupByLibrary.simpleMessage("Bind"),
|
||||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"),
|
"blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"),
|
||||||
|
"bypassDomain": MessageLookupByLibrary.simpleMessage("Bypass domain"),
|
||||||
|
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Only takes effect when the system proxy is enabled"),
|
||||||
"cancelFilterSystemApp":
|
"cancelFilterSystemApp":
|
||||||
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
|
MessageLookupByLibrary.simpleMessage("Cancel filter system app"),
|
||||||
"cancelSelectAll":
|
"cancelSelectAll":
|
||||||
@@ -130,6 +143,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"desc": MessageLookupByLibrary.simpleMessage(
|
"desc": MessageLookupByLibrary.simpleMessage(
|
||||||
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."),
|
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."),
|
||||||
"direct": MessageLookupByLibrary.simpleMessage("Direct"),
|
"direct": MessageLookupByLibrary.simpleMessage("Direct"),
|
||||||
|
"disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"),
|
||||||
|
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"This software is only used for non-commercial purposes such as learning exchanges and scientific research. It is strictly prohibited to use this software for commercial purposes. Any commercial activity, if any, has nothing to do with this software."),
|
||||||
"discoverNewVersion":
|
"discoverNewVersion":
|
||||||
MessageLookupByLibrary.simpleMessage("Discover the new version"),
|
MessageLookupByLibrary.simpleMessage("Discover the new version"),
|
||||||
"discovery":
|
"discovery":
|
||||||
@@ -152,6 +168,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
|
"expand": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||||
"expirationTime":
|
"expirationTime":
|
||||||
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
||||||
|
"exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"),
|
||||||
|
"exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"),
|
||||||
"externalController":
|
"externalController":
|
||||||
MessageLookupByLibrary.simpleMessage("ExternalController"),
|
MessageLookupByLibrary.simpleMessage("ExternalController"),
|
||||||
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
|
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
@@ -188,24 +206,36 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
"go": MessageLookupByLibrary.simpleMessage("Go"),
|
||||||
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
|
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
|
||||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"),
|
"hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"),
|
||||||
|
"hotkeyConflict":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Hotkey conflict"),
|
||||||
|
"hotkeyManagement":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Hotkey Management"),
|
||||||
|
"hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Use keyboard to control applications"),
|
||||||
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
|
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
|
||||||
|
"icon": MessageLookupByLibrary.simpleMessage("Icon"),
|
||||||
|
"iconConfiguration":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Icon configuration"),
|
||||||
|
"iconStyle": MessageLookupByLibrary.simpleMessage("Icon style"),
|
||||||
"importFromURL":
|
"importFromURL":
|
||||||
MessageLookupByLibrary.simpleMessage("Import from URL"),
|
MessageLookupByLibrary.simpleMessage("Import from URL"),
|
||||||
"infiniteTime":
|
"infiniteTime":
|
||||||
MessageLookupByLibrary.simpleMessage("Long term effective"),
|
MessageLookupByLibrary.simpleMessage("Long term effective"),
|
||||||
"init": MessageLookupByLibrary.simpleMessage("Init"),
|
"init": MessageLookupByLibrary.simpleMessage("Init"),
|
||||||
|
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Please enter the correct hotkey"),
|
||||||
"intelligentSelected":
|
"intelligentSelected":
|
||||||
MessageLookupByLibrary.simpleMessage("Intelligent selection"),
|
MessageLookupByLibrary.simpleMessage("Intelligent selection"),
|
||||||
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
|
"intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"),
|
||||||
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
|
"ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"),
|
||||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
|
||||||
"When turned on it will be able to receive IPv6 traffic"),
|
"When turned on it will be able to receive IPv6 traffic"),
|
||||||
|
"ipv6InboundDesc":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Allow IPv6 inbound"),
|
||||||
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
"just": MessageLookupByLibrary.simpleMessage("Just"),
|
||||||
"keepAliveIntervalDesc":
|
"keepAliveIntervalDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
|
MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"),
|
||||||
"key": MessageLookupByLibrary.simpleMessage("Key"),
|
"key": MessageLookupByLibrary.simpleMessage("Key"),
|
||||||
"keyNotEmpty":
|
|
||||||
MessageLookupByLibrary.simpleMessage("The key cannot be empty"),
|
|
||||||
"language": MessageLookupByLibrary.simpleMessage("Language"),
|
"language": MessageLookupByLibrary.simpleMessage("Language"),
|
||||||
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
|
"layout": MessageLookupByLibrary.simpleMessage("Layout"),
|
||||||
"light": MessageLookupByLibrary.simpleMessage("Light"),
|
"light": MessageLookupByLibrary.simpleMessage("Light"),
|
||||||
@@ -244,14 +274,22 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("Nameserver policy"),
|
MessageLookupByLibrary.simpleMessage("Nameserver policy"),
|
||||||
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage(
|
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Specify the corresponding nameserver policy"),
|
"Specify the corresponding nameserver policy"),
|
||||||
|
"network": MessageLookupByLibrary.simpleMessage("Network"),
|
||||||
|
"networkDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Modify network-related settings"),
|
||||||
"networkDetection":
|
"networkDetection":
|
||||||
MessageLookupByLibrary.simpleMessage("Network detection"),
|
MessageLookupByLibrary.simpleMessage("Network detection"),
|
||||||
"networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"),
|
"networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"),
|
||||||
|
"noData": MessageLookupByLibrary.simpleMessage("No data"),
|
||||||
|
"noHotKey": MessageLookupByLibrary.simpleMessage("No HotKey"),
|
||||||
|
"noIcon": MessageLookupByLibrary.simpleMessage("None"),
|
||||||
"noInfo": MessageLookupByLibrary.simpleMessage("No info"),
|
"noInfo": MessageLookupByLibrary.simpleMessage("No info"),
|
||||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
|
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"),
|
||||||
|
"noNetwork": MessageLookupByLibrary.simpleMessage("No network"),
|
||||||
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
|
"noProxy": MessageLookupByLibrary.simpleMessage("No proxy"),
|
||||||
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
|
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Please create a profile or add a valid profile"),
|
"Please create a profile or add a valid profile"),
|
||||||
|
"notEmpty": MessageLookupByLibrary.simpleMessage("Cannot be empty"),
|
||||||
"notSelectedTip": MessageLookupByLibrary.simpleMessage(
|
"notSelectedTip": MessageLookupByLibrary.simpleMessage(
|
||||||
"The current proxy group cannot be selected."),
|
"The current proxy group cannot be selected."),
|
||||||
"nullConnectionsDesc":
|
"nullConnectionsDesc":
|
||||||
@@ -263,6 +301,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"No profile, Please add a profile"),
|
"No profile, Please add a profile"),
|
||||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
|
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
|
||||||
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
|
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
|
||||||
|
"onlyIcon": MessageLookupByLibrary.simpleMessage("Icon"),
|
||||||
"onlyOtherApps":
|
"onlyOtherApps":
|
||||||
MessageLookupByLibrary.simpleMessage("Only third-party apps"),
|
MessageLookupByLibrary.simpleMessage("Only third-party apps"),
|
||||||
"onlyStatisticsProxy":
|
"onlyStatisticsProxy":
|
||||||
@@ -293,6 +332,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"port": MessageLookupByLibrary.simpleMessage("Port"),
|
"port": MessageLookupByLibrary.simpleMessage("Port"),
|
||||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage(
|
"preferH3Desc": MessageLookupByLibrary.simpleMessage(
|
||||||
"Prioritize the use of DOH\'s http/3"),
|
"Prioritize the use of DOH\'s http/3"),
|
||||||
|
"pressKeyboard":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Please press the keyboard."),
|
||||||
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
|
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
|
||||||
"profile": MessageLookupByLibrary.simpleMessage("Profile"),
|
"profile": MessageLookupByLibrary.simpleMessage("Profile"),
|
||||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||||
@@ -338,16 +379,18 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
|
MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
|
||||||
"recoverySuccess":
|
"recoverySuccess":
|
||||||
MessageLookupByLibrary.simpleMessage("Recovery success"),
|
MessageLookupByLibrary.simpleMessage("Recovery success"),
|
||||||
|
"regExp": MessageLookupByLibrary.simpleMessage("RegExp"),
|
||||||
"remote": MessageLookupByLibrary.simpleMessage("Remote"),
|
"remote": MessageLookupByLibrary.simpleMessage("Remote"),
|
||||||
"remoteBackupDesc":
|
"remoteBackupDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
|
MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"),
|
||||||
"remoteRecoveryDesc":
|
"remoteRecoveryDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
|
MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"),
|
||||||
|
"remove": MessageLookupByLibrary.simpleMessage("Remove"),
|
||||||
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
|
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
|
||||||
"requestsDesc": MessageLookupByLibrary.simpleMessage(
|
"requestsDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"View recently request records"),
|
"View recently request records"),
|
||||||
"reset": MessageLookupByLibrary.simpleMessage("Reset"),
|
"reset": MessageLookupByLibrary.simpleMessage("Reset"),
|
||||||
"resetDns": MessageLookupByLibrary.simpleMessage("Reset Dns"),
|
"resetTip": MessageLookupByLibrary.simpleMessage("Make sure to reset"),
|
||||||
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
|
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
|
||||||
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
|
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"External resource related info"),
|
"External resource related info"),
|
||||||
@@ -370,9 +413,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"size": MessageLookupByLibrary.simpleMessage("Size"),
|
"size": MessageLookupByLibrary.simpleMessage("Size"),
|
||||||
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
|
"sort": MessageLookupByLibrary.simpleMessage("Sort"),
|
||||||
"source": MessageLookupByLibrary.simpleMessage("Source"),
|
"source": MessageLookupByLibrary.simpleMessage("Source"),
|
||||||
|
"stackMode": MessageLookupByLibrary.simpleMessage("Stack mode"),
|
||||||
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
|
"standard": MessageLookupByLibrary.simpleMessage("Standard"),
|
||||||
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
"start": MessageLookupByLibrary.simpleMessage("Start"),
|
||||||
"startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."),
|
"startVpn": MessageLookupByLibrary.simpleMessage("Starting VPN..."),
|
||||||
"status": MessageLookupByLibrary.simpleMessage("Status"),
|
"status": MessageLookupByLibrary.simpleMessage("Status"),
|
||||||
"statusDesc": MessageLookupByLibrary.simpleMessage(
|
"statusDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"System DNS will be used when turned off"),
|
"System DNS will be used when turned off"),
|
||||||
@@ -423,8 +467,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"useSystemHosts":
|
"useSystemHosts":
|
||||||
MessageLookupByLibrary.simpleMessage("Use system hosts"),
|
MessageLookupByLibrary.simpleMessage("Use system hosts"),
|
||||||
"value": MessageLookupByLibrary.simpleMessage("Value"),
|
"value": MessageLookupByLibrary.simpleMessage("Value"),
|
||||||
"valueNotEmpty":
|
|
||||||
MessageLookupByLibrary.simpleMessage("The value cannot be empty"),
|
|
||||||
"view": MessageLookupByLibrary.simpleMessage("View"),
|
"view": MessageLookupByLibrary.simpleMessage("View"),
|
||||||
"vpnDesc":
|
"vpnDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("Modify VPN related settings"),
|
MessageLookupByLibrary.simpleMessage("Modify VPN related settings"),
|
||||||
|
|||||||
@@ -32,11 +32,20 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"account": MessageLookupByLibrary.simpleMessage("账号"),
|
"account": MessageLookupByLibrary.simpleMessage("账号"),
|
||||||
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
|
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
|
||||||
"action": MessageLookupByLibrary.simpleMessage("操作"),
|
"action": MessageLookupByLibrary.simpleMessage("操作"),
|
||||||
|
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
|
||||||
|
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
|
||||||
|
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
|
||||||
|
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
||||||
|
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
|
||||||
"add": MessageLookupByLibrary.simpleMessage("添加"),
|
"add": MessageLookupByLibrary.simpleMessage("添加"),
|
||||||
"address": MessageLookupByLibrary.simpleMessage("地址"),
|
"address": MessageLookupByLibrary.simpleMessage("地址"),
|
||||||
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
||||||
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
||||||
|
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
|
||||||
|
"adminAutoLaunchDesc":
|
||||||
|
MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
|
||||||
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
||||||
|
"agree": MessageLookupByLibrary.simpleMessage("同意"),
|
||||||
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
|
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
|
||||||
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
|
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
|
||||||
"allowBypassDesc":
|
"allowBypassDesc":
|
||||||
@@ -69,6 +78,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
|
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
|
||||||
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
|
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
|
||||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
||||||
|
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
|
||||||
|
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
|
||||||
"cancelFilterSystemApp":
|
"cancelFilterSystemApp":
|
||||||
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
|
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
|
||||||
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
|
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
|
||||||
@@ -107,6 +118,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"desc": MessageLookupByLibrary.simpleMessage(
|
"desc": MessageLookupByLibrary.simpleMessage(
|
||||||
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。"),
|
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。"),
|
||||||
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
||||||
|
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
|
||||||
|
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。"),
|
||||||
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||||
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||||
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
|
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
|
||||||
@@ -123,6 +137,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||||
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
||||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||||
|
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||||
|
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
||||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||||
"externalControllerDesc":
|
"externalControllerDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
|
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
|
||||||
@@ -151,19 +167,27 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
||||||
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
||||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
||||||
|
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
|
||||||
|
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
|
||||||
|
"hotkeyManagementDesc":
|
||||||
|
MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
|
||||||
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
||||||
|
"icon": MessageLookupByLibrary.simpleMessage("图片"),
|
||||||
|
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
|
||||||
|
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
|
||||||
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
||||||
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
|
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
|
||||||
"init": MessageLookupByLibrary.simpleMessage("初始化"),
|
"init": MessageLookupByLibrary.simpleMessage("初始化"),
|
||||||
|
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
|
||||||
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
|
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
|
||||||
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
||||||
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
||||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
||||||
|
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
|
||||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||||
"keepAliveIntervalDesc":
|
"keepAliveIntervalDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
||||||
"key": MessageLookupByLibrary.simpleMessage("键"),
|
"key": MessageLookupByLibrary.simpleMessage("键"),
|
||||||
"keyNotEmpty": MessageLookupByLibrary.simpleMessage("键不能为空"),
|
|
||||||
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
||||||
"layout": MessageLookupByLibrary.simpleMessage("布局"),
|
"layout": MessageLookupByLibrary.simpleMessage("布局"),
|
||||||
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
||||||
@@ -178,7 +202,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
|
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
|
||||||
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
|
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
|
||||||
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
|
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
|
||||||
"loose": MessageLookupByLibrary.simpleMessage("紧凑"),
|
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
|
||||||
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
||||||
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
|
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
|
||||||
"minimizeOnExitDesc":
|
"minimizeOnExitDesc":
|
||||||
@@ -194,13 +218,20 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
|
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
|
||||||
"nameserverPolicyDesc":
|
"nameserverPolicyDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
|
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
|
||||||
|
"network": MessageLookupByLibrary.simpleMessage("网络"),
|
||||||
|
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
|
||||||
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
|
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
|
||||||
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
|
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
|
||||||
|
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
|
||||||
|
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
|
||||||
|
"noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
|
||||||
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
||||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
||||||
|
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
|
||||||
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||||
"noProxyDesc":
|
"noProxyDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
||||||
|
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
|
||||||
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
|
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
|
||||||
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
|
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
|
||||||
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
|
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
|
||||||
@@ -209,6 +240,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
||||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
||||||
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
||||||
|
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
|
||||||
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
|
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
|
||||||
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
|
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
|
||||||
"onlyStatisticsProxyDesc":
|
"onlyStatisticsProxyDesc":
|
||||||
@@ -231,6 +263,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
|
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
|
||||||
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
||||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
|
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
|
||||||
|
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
|
||||||
"preview": MessageLookupByLibrary.simpleMessage("预览"),
|
"preview": MessageLookupByLibrary.simpleMessage("预览"),
|
||||||
"profile": MessageLookupByLibrary.simpleMessage("配置"),
|
"profile": MessageLookupByLibrary.simpleMessage("配置"),
|
||||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||||
@@ -265,14 +298,16 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
|
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
|
||||||
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
|
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
|
||||||
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
|
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
|
||||||
|
"regExp": MessageLookupByLibrary.simpleMessage("正则"),
|
||||||
"remote": MessageLookupByLibrary.simpleMessage("远程"),
|
"remote": MessageLookupByLibrary.simpleMessage("远程"),
|
||||||
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
|
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
|
||||||
"remoteRecoveryDesc":
|
"remoteRecoveryDesc":
|
||||||
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
|
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
|
||||||
|
"remove": MessageLookupByLibrary.simpleMessage("移除"),
|
||||||
"requests": MessageLookupByLibrary.simpleMessage("请求"),
|
"requests": MessageLookupByLibrary.simpleMessage("请求"),
|
||||||
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
|
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
|
||||||
"reset": MessageLookupByLibrary.simpleMessage("重置"),
|
"reset": MessageLookupByLibrary.simpleMessage("重置"),
|
||||||
"resetDns": MessageLookupByLibrary.simpleMessage("重置DNS"),
|
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
|
||||||
"resources": MessageLookupByLibrary.simpleMessage("资源"),
|
"resources": MessageLookupByLibrary.simpleMessage("资源"),
|
||||||
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
||||||
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
|
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
|
||||||
@@ -293,6 +328,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
|
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
|
||||||
"sort": MessageLookupByLibrary.simpleMessage("排序"),
|
"sort": MessageLookupByLibrary.simpleMessage("排序"),
|
||||||
"source": MessageLookupByLibrary.simpleMessage("来源"),
|
"source": MessageLookupByLibrary.simpleMessage("来源"),
|
||||||
|
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
|
||||||
"standard": MessageLookupByLibrary.simpleMessage("标准"),
|
"standard": MessageLookupByLibrary.simpleMessage("标准"),
|
||||||
"start": MessageLookupByLibrary.simpleMessage("启动"),
|
"start": MessageLookupByLibrary.simpleMessage("启动"),
|
||||||
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
|
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
|
||||||
@@ -318,7 +354,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
|
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
|
||||||
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
|
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
|
||||||
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
|
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
|
||||||
"tight": MessageLookupByLibrary.simpleMessage("宽松"),
|
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
|
||||||
"time": MessageLookupByLibrary.simpleMessage("时间"),
|
"time": MessageLookupByLibrary.simpleMessage("时间"),
|
||||||
"tip": MessageLookupByLibrary.simpleMessage("提示"),
|
"tip": MessageLookupByLibrary.simpleMessage("提示"),
|
||||||
"tools": MessageLookupByLibrary.simpleMessage("工具"),
|
"tools": MessageLookupByLibrary.simpleMessage("工具"),
|
||||||
@@ -338,7 +374,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
|
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
|
||||||
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
|
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
|
||||||
"value": MessageLookupByLibrary.simpleMessage("值"),
|
"value": MessageLookupByLibrary.simpleMessage("值"),
|
||||||
"valueNotEmpty": MessageLookupByLibrary.simpleMessage("值不能为空"),
|
|
||||||
"view": MessageLookupByLibrary.simpleMessage("查看"),
|
"view": MessageLookupByLibrary.simpleMessage("查看"),
|
||||||
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
|
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
|
||||||
"vpnEnableDesc":
|
"vpnEnableDesc":
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user