Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8282a9a474 | ||
|
|
dfa6d31673 | ||
|
|
89bbbc6864 | ||
|
|
a3e1b38201 | ||
|
|
4e3dc45f13 | ||
|
|
13d31cf708 | ||
|
|
62a7772b92 | ||
|
|
043648f998 | ||
|
|
3eb26e8061 | ||
|
|
5d6bd6466f | ||
|
|
4e766d9407 | ||
|
|
80f8aa22ee | ||
|
|
97714e8b25 | ||
|
|
50bf4170d9 | ||
|
|
79efa67df3 | ||
|
|
ac397393a0 | ||
|
|
b685165230 | ||
|
|
402221aaa2 | ||
|
|
f6d9ed11d9 | ||
|
|
c38a671d57 | ||
|
|
75af47aead | ||
|
|
8dafe3b0ec | ||
|
|
813198a21d | ||
|
|
68dd262fef | ||
|
|
5ef020db73 | ||
|
|
e3c9035903 | ||
|
|
7fc54c5295 | ||
|
|
00a78b5fb4 | ||
|
|
8cdaf30de0 | ||
|
|
f39b9cf933 | ||
|
|
9df1ff46c2 | ||
|
|
fcbbbdc698 | ||
|
|
3ba8355772 | ||
|
|
f6b97f82ae | ||
|
|
13ac20f273 |
5
.github/release_template.md
vendored
5
.github/release_template.md
vendored
@@ -31,10 +31,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>macOS</td>
|
<td>macOS (v10.15+)</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-arm64.dmg"><img src="https://img.shields.io/badge/DMG-Apple%20Silicon-%23000000.svg?logo=apple"></a><br>
|
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Universal-ea005e.svg?logo=apple"></a><br>
|
||||||
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Intel%20X64-%2300A9E0.svg?logo=apple"></a><br>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
87
.github/workflows/build.yaml
vendored
87
.github/workflows/build.yaml
vendored
@@ -27,6 +27,25 @@ jobs:
|
|||||||
arch: arm64
|
arch: arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup Mingw64
|
||||||
|
if: startsWith(matrix.platform,'windows')
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
msystem: mingw64
|
||||||
|
install: mingw-w64-x86_64-gcc
|
||||||
|
update: true
|
||||||
|
|
||||||
|
- name: Set Mingw64 Env
|
||||||
|
if: startsWith(matrix.platform,'windows')
|
||||||
|
run: |
|
||||||
|
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
|
- name: Check Matrix
|
||||||
|
run: |
|
||||||
|
echo "Running on ${{ matrix.os }}"
|
||||||
|
echo "Arch: ${{ runner.arch }}"
|
||||||
|
gcc --version
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -60,7 +79,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version-file: 'core/go.mod'
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
core/go.sum
|
core/go.sum
|
||||||
|
|
||||||
@@ -84,69 +103,6 @@ jobs:
|
|||||||
path: ./dist
|
path: ./dist
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
changelog:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ build ]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
if: ${{ !contains(github.ref, '+') }}
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: refs/heads/main
|
|
||||||
|
|
||||||
- name: Generate
|
|
||||||
if: ${{ !contains(github.ref, '+') }}
|
|
||||||
run: |
|
|
||||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
|
||||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
|
||||||
currentTag=""
|
|
||||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
|
||||||
if (( i < ${#tags[@]} )); then
|
|
||||||
tag=${tags[$i]}
|
|
||||||
else
|
|
||||||
tag=""
|
|
||||||
fi
|
|
||||||
if [ -n "$currentTag" ]; then
|
|
||||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -n "$currentTag" ]; then
|
|
||||||
echo "## $currentTag" >> NEW_CHANGELOG.md
|
|
||||||
echo "" >> NEW_CHANGELOG.md
|
|
||||||
if [ -n "$tag" ]; then
|
|
||||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
|
||||||
else
|
|
||||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
|
||||||
fi
|
|
||||||
echo "" >> NEW_CHANGELOG.md
|
|
||||||
fi
|
|
||||||
currentTag=$tag
|
|
||||||
done
|
|
||||||
cat CHANGELOG.md >> NEW_CHANGELOG.md
|
|
||||||
cat NEW_CHANGELOG.md > CHANGELOG.md
|
|
||||||
|
|
||||||
- name: Commit
|
|
||||||
if: ${{ !contains(github.ref, '+') }}
|
|
||||||
run: |
|
|
||||||
git add CHANGELOG.md
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
echo "Commit pushing"
|
|
||||||
git config --local user.email "chen08209@gmail.com"
|
|
||||||
git config --local user.name "chen08209"
|
|
||||||
git commit -m "Update changelog"
|
|
||||||
git push
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Push succeeded"
|
|
||||||
else
|
|
||||||
echo "Push failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
upload:
|
upload:
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
@@ -241,5 +197,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
.gitignore
vendored
2
.gitignore
vendored
@@ -5,11 +5,9 @@
|
|||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
.atom/
|
||||||
.build/
|
|
||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
.swiftpm/
|
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
|
|||||||
100
CHANGELOG.md
100
CHANGELOG.md
@@ -1,88 +1,4 @@
|
|||||||
## v0.8.70
|
## v0.8.63
|
||||||
|
|
||||||
- Support better window position memory
|
|
||||||
|
|
||||||
- Add windows arm64 and linux arm64 build script
|
|
||||||
|
|
||||||
- Optimize some details
|
|
||||||
|
|
||||||
## v0.8.69
|
|
||||||
|
|
||||||
- Remake desktop
|
|
||||||
|
|
||||||
- Optimize change proxy
|
|
||||||
|
|
||||||
- Optimize network check
|
|
||||||
|
|
||||||
- Fix fallback issues
|
|
||||||
|
|
||||||
- Optimize lots of details
|
|
||||||
|
|
||||||
- Update change.yaml
|
|
||||||
|
|
||||||
- Fix android tile issues
|
|
||||||
|
|
||||||
- Fix windows tray issues
|
|
||||||
|
|
||||||
- Support setting bypassDomain
|
|
||||||
|
|
||||||
- Update flutter version
|
|
||||||
|
|
||||||
- Fix android service issues
|
|
||||||
|
|
||||||
- Fix macos dock exit button issues
|
|
||||||
|
|
||||||
- Add route address setting
|
|
||||||
|
|
||||||
- Optimize provider view
|
|
||||||
|
|
||||||
- Update changelog
|
|
||||||
|
|
||||||
- Update CHANGELOG.md
|
|
||||||
|
|
||||||
## v0.8.67
|
|
||||||
|
|
||||||
- Add android shortcuts
|
|
||||||
|
|
||||||
- Fix init params issues
|
|
||||||
|
|
||||||
- Fix dynamic color issues
|
|
||||||
|
|
||||||
- Optimize navigator animate
|
|
||||||
|
|
||||||
- Optimize window init
|
|
||||||
|
|
||||||
- Optimize fab
|
|
||||||
|
|
||||||
- Optimize save
|
|
||||||
|
|
||||||
## v0.8.66
|
|
||||||
|
|
||||||
- Fix the collapse issues
|
|
||||||
|
|
||||||
- Add fontFamily options
|
|
||||||
|
|
||||||
## v0.8.65
|
|
||||||
|
|
||||||
- Update core version
|
|
||||||
|
|
||||||
- Update flutter version
|
|
||||||
|
|
||||||
- Optimize ip check
|
|
||||||
|
|
||||||
- Optimize url-test
|
|
||||||
|
|
||||||
## v0.8.64
|
|
||||||
|
|
||||||
- Update release message
|
|
||||||
|
|
||||||
- Init auto gen changelog
|
|
||||||
|
|
||||||
- Fix windows tray issues
|
|
||||||
|
|
||||||
- Fix urltest issues
|
|
||||||
|
|
||||||
- Add auto changelog
|
|
||||||
|
|
||||||
- Fix windows admin auto launch issues
|
- Fix windows admin auto launch issues
|
||||||
|
|
||||||
@@ -94,6 +10,8 @@
|
|||||||
|
|
||||||
- Fix some issues
|
- Fix some issues
|
||||||
|
|
||||||
|
## v0.8.62
|
||||||
|
|
||||||
- Optimize ip detection
|
- Optimize ip detection
|
||||||
|
|
||||||
- Support android vpn ipv6 inbound switch
|
- Support android vpn ipv6 inbound switch
|
||||||
@@ -110,6 +28,12 @@
|
|||||||
|
|
||||||
- Update readme
|
- Update readme
|
||||||
|
|
||||||
|
- Update README.md 2
|
||||||
|
|
||||||
|
- Update README.md 2
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
## v0.8.60
|
## v0.8.60
|
||||||
|
|
||||||
- Fix build error2
|
- Fix build error2
|
||||||
@@ -537,8 +461,7 @@
|
|||||||
|
|
||||||
## v0.8.12
|
## v0.8.12
|
||||||
|
|
||||||
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the
|
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
|
||||||
application to flash back.
|
|
||||||
|
|
||||||
- Fix edit profile error
|
- Fix edit profile error
|
||||||
|
|
||||||
@@ -703,4 +626,5 @@
|
|||||||
|
|
||||||
- update mobile_scanner
|
- update mobile_scanner
|
||||||
|
|
||||||
- Initial commit
|
- Initial commit
|
||||||
|
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -34,29 +34,6 @@ on Mobile:
|
|||||||
|
|
||||||
✨ Support subscription link, Dark mode
|
✨ Support subscription link, Dark mode
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
⚠️ Make sure to install the following dependencies before using them
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install appindicator3-0.1 libappindicator3-dev
|
|
||||||
sudo apt-get install keybinder-3.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
Support the following actions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
com.follow.clash.action.START
|
|
||||||
|
|
||||||
com.follow.clash.action.STOP
|
|
||||||
|
|
||||||
com.follow.clash.action.CHANGE
|
|
||||||
```
|
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
<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>
|
||||||
@@ -93,7 +70,7 @@ Support the following actions
|
|||||||
3. Run build script
|
3. Run build script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dart .\setup.dart windows --arch <arm64 | amd64>
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
- linux
|
- linux
|
||||||
@@ -103,7 +80,7 @@ Support the following actions
|
|||||||
2. Run build script
|
2. Run build script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dart .\setup.dart linux --arch <arm64 | amd64>
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
- macOS
|
- macOS
|
||||||
@@ -113,7 +90,7 @@ Support the following actions
|
|||||||
2. Run build script
|
2. Run build script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dart .\setup.dart macos --arch <arm64 | amd64>
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Star
|
## Star
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
[](https://t.me/FlClash)
|
[](https://t.me/FlClash)
|
||||||
|
|
||||||
|
|
||||||
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
||||||
|
|
||||||
on Desktop:
|
on Desktop:
|
||||||
@@ -34,29 +35,6 @@ on Mobile:
|
|||||||
|
|
||||||
✨ 支持一键导入订阅, 深色模式
|
✨ 支持一键导入订阅, 深色模式
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
⚠️ 使用前请确保安装以下依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install appindicator3-0.1 libappindicator3-dev
|
|
||||||
sudo apt-get install keybinder-3.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
支持下列操作
|
|
||||||
|
|
||||||
```bash
|
|
||||||
com.follow.clash.action.START
|
|
||||||
|
|
||||||
com.follow.clash.action.STOP
|
|
||||||
|
|
||||||
com.follow.clash.action.CHANGE
|
|
||||||
```
|
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
<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>
|
||||||
@@ -93,7 +71,7 @@ on Mobile:
|
|||||||
3. 运行构建脚本
|
3. 运行构建脚本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dart .\setup.dart windows --arch <arm64 | amd64>
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
- linux
|
- linux
|
||||||
@@ -103,7 +81,7 @@ on Mobile:
|
|||||||
2. 运行构建脚本
|
2. 运行构建脚本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dart .\setup.dart linux --arch <arm64 | amd64>
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
- macOS
|
- macOS
|
||||||
@@ -113,7 +91,7 @@ on Mobile:
|
|||||||
2. 运行构建脚本
|
2. 运行构建脚本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dart .\setup.dart macos --arch <arm64 | amd64>
|
dart .\setup.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
|
|
||||||
<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
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
|
||||||
tools:ignore="SystemPermissionTypo" />
|
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" />
|
||||||
@@ -24,8 +23,8 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:label="FlClash">
|
android:label="FlClash">
|
||||||
<activity
|
<activity
|
||||||
android:name="com.follow.clash.MainActivity"
|
android:name="com.follow.clash.MainActivity"
|
||||||
@@ -74,15 +73,11 @@
|
|||||||
android:theme="@style/TransparentTheme">
|
android:theme="@style/TransparentTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<action android:name="${applicationId}.action.START" />
|
<action android:name="com.follow.clash.action.START" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<action android:name="${applicationId}.action.STOP" />
|
<action android:name="com.follow.clash.action.STOP" />
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<action android:name="${applicationId}.action.CHANGE" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@@ -92,8 +87,7 @@
|
|||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:label="FlClash"
|
android:label="FlClash"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
tools:targetApi="n">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
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.TilePlugin
|
|
||||||
import com.follow.clash.plugins.VpnPlugin
|
import com.follow.clash.plugins.VpnPlugin
|
||||||
|
import com.follow.clash.plugins.TilePlugin
|
||||||
import io.flutter.FlutterInjector
|
import io.flutter.FlutterInjector
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
@@ -20,6 +20,8 @@ enum class RunState {
|
|||||||
|
|
||||||
|
|
||||||
object GlobalState {
|
object GlobalState {
|
||||||
|
|
||||||
|
private val lock = ReentrantLock()
|
||||||
val runLock = ReentrantLock()
|
val runLock = ReentrantLock()
|
||||||
|
|
||||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||||
@@ -31,10 +33,6 @@ object GlobalState {
|
|||||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getText(text: String): String {
|
|
||||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentTilePlugin(): 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?
|
||||||
@@ -44,47 +42,15 @@ object GlobalState {
|
|||||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleToggle(context: Context) {
|
|
||||||
val starting = handleStart(context)
|
|
||||||
if (!starting) {
|
|
||||||
handleStop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleStart(context: Context): Boolean {
|
|
||||||
if (runState.value == RunState.STOP) {
|
|
||||||
runState.value = RunState.PENDING
|
|
||||||
runLock.lock()
|
|
||||||
val tilePlugin = getCurrentTilePlugin()
|
|
||||||
if (tilePlugin != null) {
|
|
||||||
tilePlugin.handleStart()
|
|
||||||
} else {
|
|
||||||
initServiceEngine(context)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleStop() {
|
|
||||||
if (runState.value == RunState.START) {
|
|
||||||
runState.value = RunState.PENDING
|
|
||||||
runLock.lock()
|
|
||||||
getCurrentTilePlugin()?.handleStop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroyServiceEngine() {
|
fun destroyServiceEngine() {
|
||||||
runLock.withLock {
|
serviceEngine?.destroy()
|
||||||
serviceEngine?.destroy()
|
serviceEngine = null
|
||||||
serviceEngine = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initServiceEngine(context: Context) {
|
fun initServiceEngine(context: Context) {
|
||||||
if (serviceEngine != null) return
|
if (serviceEngine != null) return
|
||||||
destroyServiceEngine()
|
lock.withLock {
|
||||||
runLock.withLock {
|
destroyServiceEngine()
|
||||||
serviceEngine = FlutterEngine(context)
|
serviceEngine = FlutterEngine(context)
|
||||||
serviceEngine?.plugins?.add(VpnPlugin())
|
serviceEngine?.plugins?.add(VpnPlugin())
|
||||||
serviceEngine?.plugins?.add(AppPlugin())
|
serviceEngine?.plugins?.add(AppPlugin())
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.follow.clash
|
|||||||
|
|
||||||
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.TilePlugin
|
|
||||||
import com.follow.clash.plugins.VpnPlugin
|
import com.follow.clash.plugins.VpnPlugin
|
||||||
|
import com.follow.clash.plugins.TilePlugin
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,17 @@ package com.follow.clash
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.follow.clash.extensions.wrapAction
|
|
||||||
|
|
||||||
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) {
|
when (intent.action) {
|
||||||
wrapAction("START") -> {
|
"com.follow.clash.action.START" -> {
|
||||||
GlobalState.handleStart(applicationContext)
|
GlobalState.getCurrentTilePlugin()?.handleStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapAction("STOP") -> {
|
"com.follow.clash.action.STOP" -> {
|
||||||
GlobalState.handleStop()
|
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||||
}
|
|
||||||
|
|
||||||
wrapAction("CHANGE") -> {
|
|
||||||
GlobalState.handleToggle(applicationContext)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
package com.follow.clash.extensions
|
package com.follow.clash.extensions
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.os.Build
|
|
||||||
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.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.follow.clash.TempActivity
|
|
||||||
import com.follow.clash.models.CIDR
|
import com.follow.clash.models.CIDR
|
||||||
import com.follow.clash.models.Metadata
|
import com.follow.clash.models.Metadata
|
||||||
import com.follow.clash.models.VpnOptions
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
import java.net.Inet6Address
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun Drawable.getBase64(): String {
|
suspend fun Drawable.getBase64(): String {
|
||||||
@@ -44,40 +34,6 @@ fun Metadata.getProtocol(): Int? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
|
|
||||||
return routeAddress.filter {
|
|
||||||
it.isIpv4()
|
|
||||||
}.map {
|
|
||||||
it.toCIDR()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
|
|
||||||
return routeAddress.filter {
|
|
||||||
it.isIpv6()
|
|
||||||
}.map {
|
|
||||||
it.toCIDR()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.isIpv4(): Boolean {
|
|
||||||
val parts = split("/")
|
|
||||||
if (parts.size != 2) {
|
|
||||||
throw IllegalArgumentException("Invalid CIDR format")
|
|
||||||
}
|
|
||||||
val address = InetAddress.getByName(parts[0])
|
|
||||||
return address.address.size == 4
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.isIpv6(): Boolean {
|
|
||||||
val parts = split("/")
|
|
||||||
if (parts.size != 2) {
|
|
||||||
throw IllegalArgumentException("Invalid CIDR format")
|
|
||||||
}
|
|
||||||
val address = InetAddress.getByName(parts[0])
|
|
||||||
return address.address.size == 16
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.toCIDR(): CIDR {
|
fun String.toCIDR(): CIDR {
|
||||||
val parts = split("/")
|
val parts = split("/")
|
||||||
if (parts.size != 2) {
|
if (parts.size != 2) {
|
||||||
@@ -115,34 +71,6 @@ fun InetAddress.asSocketAddressText(port: Int): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.wrapAction(action: String): String {
|
|
||||||
return "${this.packageName}.action.$action"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.getActionIntent(action: String): Intent {
|
|
||||||
val actionIntent = Intent(this, TempActivity::class.java)
|
|
||||||
actionIntent.action = wrapAction(action)
|
|
||||||
return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.getActionPendingIntent(action: String): PendingIntent {
|
|
||||||
return if (Build.VERSION.SDK_INT >= 31) {
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
getActionIntent(action),
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
getActionIntent(action),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun numericToTextFormat(src: ByteArray): String {
|
private fun numericToTextFormat(src: ByteArray): String {
|
||||||
val sb = StringBuilder(39)
|
val sb = StringBuilder(39)
|
||||||
@@ -159,40 +87,3 @@ private fun numericToTextFormat(src: ByteArray): String {
|
|||||||
}
|
}
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> MethodChannel.awaitResult(
|
|
||||||
method: String,
|
|
||||||
arguments: Any? = null
|
|
||||||
): T? = withContext(Dispatchers.Main) { // 切换到主线程
|
|
||||||
suspendCoroutine { continuation ->
|
|
||||||
invokeMethod(method, arguments, object : MethodChannel.Result {
|
|
||||||
override fun success(result: Any?) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
continuation.resume(result as T)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun error(code: String, message: String?, details: Any?) {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notImplemented() {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ReentrantLock.safeLock() {
|
|
||||||
if (this.isLocked) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ReentrantLock.safeUnlock() {
|
|
||||||
if (!this.isLocked) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unlock()
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,8 @@ package com.follow.clash.models
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
enum class AccessControlMode {
|
enum class AccessControlMode {
|
||||||
acceptSelected, rejectSelected,
|
acceptSelected,
|
||||||
|
rejectSelected,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AccessControl(
|
data class AccessControl(
|
||||||
@@ -21,7 +22,6 @@ data class VpnOptions(
|
|||||||
val allowBypass: Boolean,
|
val allowBypass: Boolean,
|
||||||
val systemProxy: Boolean,
|
val systemProxy: Boolean,
|
||||||
val bypassDomain: List<String>,
|
val bypassDomain: List<String>,
|
||||||
val routeAddress: List<String>,
|
|
||||||
val ipv4Address: String,
|
val ipv4Address: String,
|
||||||
val ipv6Address: String,
|
val ipv6Address: String,
|
||||||
val dnsServerAddress: String,
|
val dnsServerAddress: String,
|
||||||
|
|||||||
@@ -14,15 +14,9 @@ import android.widget.Toast
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import com.follow.clash.GlobalState
|
import com.follow.clash.GlobalState
|
||||||
import com.follow.clash.R
|
|
||||||
import com.follow.clash.extensions.awaitResult
|
|
||||||
import com.follow.clash.extensions.getActionIntent
|
|
||||||
import com.follow.clash.extensions.getBase64
|
import com.follow.clash.extensions.getBase64
|
||||||
import com.follow.clash.models.Package
|
import com.follow.clash.models.Package
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
@@ -37,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
@@ -46,6 +39,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
|
|
||||||
|
private var toast: Toast? = null
|
||||||
|
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
|
|
||||||
private lateinit var channel: MethodChannel
|
private lateinit var channel: MethodChannel
|
||||||
@@ -121,21 +116,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
scope = CoroutineScope(Dispatchers.Default)
|
scope = CoroutineScope(Dispatchers.Default)
|
||||||
context = flutterPluginBinding.applicationContext
|
context = flutterPluginBinding.applicationContext;
|
||||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||||
channel.setMethodCallHandler(this)
|
channel.setMethodCallHandler(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initShortcuts(label: String) {
|
|
||||||
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
|
|
||||||
.setShortLabel(label)
|
|
||||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
|
|
||||||
.setIntent(context.getActionIntent("CHANGE"))
|
|
||||||
.build()
|
|
||||||
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
channel.setMethodCallHandler(null)
|
channel.setMethodCallHandler(null)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
@@ -143,7 +128,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
private fun tip(message: String?) {
|
private fun tip(message: String?) {
|
||||||
if (GlobalState.flutterEngine == null) {
|
if (GlobalState.flutterEngine == null) {
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
if (toast != null) {
|
||||||
|
toast!!.cancel()
|
||||||
|
}
|
||||||
|
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||||
|
toast!!.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,18 +140,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
when (call.method) {
|
when (call.method) {
|
||||||
"moveTaskToBack" -> {
|
"moveTaskToBack" -> {
|
||||||
activity?.moveTaskToBack(true)
|
activity?.moveTaskToBack(true)
|
||||||
result.success(true)
|
result.success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
"updateExcludeFromRecents" -> {
|
"updateExcludeFromRecents" -> {
|
||||||
val value = call.argument<Boolean>("value")
|
val value = call.argument<Boolean>("value")
|
||||||
updateExcludeFromRecents(value)
|
updateExcludeFromRecents(value)
|
||||||
result.success(true)
|
result.success(true);
|
||||||
}
|
|
||||||
|
|
||||||
"initShortcuts" -> {
|
|
||||||
initShortcuts(call.arguments as String)
|
|
||||||
result.success(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"getPackages" -> {
|
"getPackages" -> {
|
||||||
@@ -213,7 +197,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
result.notImplemented()
|
result.notImplemented();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,7 +270,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
|
|
||||||
private fun getPackages(): List<Package> {
|
private fun getPackages(): List<Package> {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
if (packages.isNotEmpty()) return packages
|
if (packages.isNotEmpty()) return packages;
|
||||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||||
it.packageName != context.packageName
|
it.packageName != context.packageName
|
||||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||||
@@ -300,7 +284,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
firstInstallTime = it.firstInstallTime
|
firstInstallTime = it.firstInstallTime
|
||||||
)
|
)
|
||||||
}?.let { packages.addAll(it) }
|
}?.let { packages.addAll(it) }
|
||||||
return packages
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPackagesToJson(): String {
|
private suspend fun getPackagesToJson(): String {
|
||||||
@@ -322,7 +306,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
val intent = VpnService.prepare(context)
|
val intent = VpnService.prepare(context)
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
vpnCallBack?.invoke()
|
vpnCallBack?.invoke()
|
||||||
}
|
}
|
||||||
@@ -346,12 +330,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getText(text: String): String? {
|
|
||||||
return runBlocking {
|
|
||||||
channel.awaitResult<String>("getText", text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -420,7 +398,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activity = binding.activity;
|
||||||
binding.addActivityResultListener(::onActivityResult)
|
binding.addActivityResultListener(::onActivityResult)
|
||||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||||
}
|
}
|
||||||
@@ -430,7 +408,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
activity = binding.activity
|
activity = binding.activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {
|
override fun onDetachedFromActivity() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import com.follow.clash.GlobalState
|
|||||||
import com.follow.clash.RunState
|
import com.follow.clash.RunState
|
||||||
import com.follow.clash.extensions.getProtocol
|
import com.follow.clash.extensions.getProtocol
|
||||||
import com.follow.clash.extensions.resolveDns
|
import com.follow.clash.extensions.resolveDns
|
||||||
import com.follow.clash.models.Process
|
|
||||||
import com.follow.clash.models.VpnOptions
|
|
||||||
import com.follow.clash.services.FlClashService
|
import com.follow.clash.services.FlClashService
|
||||||
import com.follow.clash.services.FlClashVpnService
|
import com.follow.clash.services.FlClashVpnService
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
@@ -30,6 +28,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.InetSocketAddress
|
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 {
|
||||||
@@ -111,9 +111,11 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
|
|
||||||
"resolverProcess" -> {
|
"resolverProcess" -> {
|
||||||
val data = call.argument<String>("data")
|
val data = call.argument<String>("data")
|
||||||
val process = if (data != null) Gson().fromJson(
|
val process =
|
||||||
data, Process::class.java
|
if (data != null) Gson().fromJson(
|
||||||
) else null
|
data,
|
||||||
|
Process::class.java
|
||||||
|
) else null
|
||||||
val metadata = process?.metadata
|
val metadata = process?.metadata
|
||||||
if (metadata == null) {
|
if (metadata == null) {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
@@ -171,7 +173,9 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
fun onUpdateNetwork() {
|
fun onUpdateNetwork() {
|
||||||
val dns = networks.flatMap { network ->
|
val dns = networks.flatMap { network ->
|
||||||
connectivity?.resolveDns(network) ?: emptyList()
|
connectivity?.resolveDns(network) ?: emptyList()
|
||||||
}.toSet().joinToString(",")
|
}
|
||||||
|
.toSet()
|
||||||
|
.joinToString(",")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
flutterMethodChannel.invokeMethod("dnsChanged", dns)
|
||||||
@@ -235,7 +239,8 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
GlobalState.runState.value = RunState.START
|
GlobalState.runState.value = RunState.START
|
||||||
val fd = flClashService?.start(options)
|
val fd = flClashService?.start(options)
|
||||||
flutterMethodChannel.invokeMethod(
|
flutterMethodChannel.invokeMethod(
|
||||||
"started", fd
|
"started",
|
||||||
|
fd
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,8 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
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.MainActivity
|
import com.follow.clash.MainActivity
|
||||||
import com.follow.clash.extensions.getActionPendingIntent
|
|
||||||
import com.follow.clash.models.VpnOptions
|
import com.follow.clash.models.VpnOptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
class FlClashService : Service(), BaseServiceInterface {
|
class FlClashService : Service(), BaseServiceInterface {
|
||||||
@@ -69,11 +64,6 @@ class FlClashService : Service(), BaseServiceInterface {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||||
}
|
}
|
||||||
addAction(
|
|
||||||
0,
|
|
||||||
GlobalState.getText("stop"),
|
|
||||||
getActionPendingIntent("STOP")
|
|
||||||
)
|
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
@@ -92,23 +82,21 @@ class FlClashService : Service(), BaseServiceInterface {
|
|||||||
|
|
||||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||||
override fun startForeground(title: String, content: String) {
|
override fun startForeground(title: String, content: String) {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
if (channel == null) {
|
||||||
if (channel == null) {
|
channel =
|
||||||
channel =
|
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
manager?.createNotificationChannel(channel)
|
||||||
manager?.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val notification =
|
|
||||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
|
||||||
} else {
|
|
||||||
startForeground(notificationId, notification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val notification =
|
||||||
|
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
} else {
|
||||||
|
startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ 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
|
||||||
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -66,7 +67,19 @@ class FlClashTileService : TileService() {
|
|||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
activityTransfer()
|
activityTransfer()
|
||||||
GlobalState.handleToggle(applicationContext)
|
if (GlobalState.runState.value == RunState.STOP) {
|
||||||
|
GlobalState.runState.value = RunState.PENDING
|
||||||
|
val tilePlugin = GlobalState.getCurrentTilePlugin()
|
||||||
|
if (tilePlugin != null) {
|
||||||
|
tilePlugin.handleStart()
|
||||||
|
} else {
|
||||||
|
GlobalState.initServiceEngine(applicationContext)
|
||||||
|
}
|
||||||
|
} else if (GlobalState.runState.value == RunState.START) {
|
||||||
|
GlobalState.runState.value = RunState.PENDING
|
||||||
|
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -15,15 +15,12 @@ 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.extensions.getActionPendingIntent
|
import com.follow.clash.TempActivity
|
||||||
import com.follow.clash.extensions.getIpv4RouteAddress
|
|
||||||
import com.follow.clash.extensions.getIpv6RouteAddress
|
|
||||||
import com.follow.clash.extensions.toCIDR
|
import com.follow.clash.extensions.toCIDR
|
||||||
import com.follow.clash.models.AccessControlMode
|
import com.follow.clash.models.AccessControlMode
|
||||||
import com.follow.clash.models.VpnOptions
|
import com.follow.clash.models.VpnOptions
|
||||||
@@ -43,28 +40,12 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
if (options.ipv4Address.isNotEmpty()) {
|
if (options.ipv4Address.isNotEmpty()) {
|
||||||
val cidr = options.ipv4Address.toCIDR()
|
val cidr = options.ipv4Address.toCIDR()
|
||||||
addAddress(cidr.address, cidr.prefixLength)
|
addAddress(cidr.address, cidr.prefixLength)
|
||||||
val routeAddress = options.getIpv4RouteAddress()
|
addRoute("0.0.0.0", 0)
|
||||||
if (routeAddress.isNotEmpty()) {
|
|
||||||
routeAddress.forEach { i ->
|
|
||||||
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
|
|
||||||
addRoute(i.address, i.prefixLength)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addRoute("0.0.0.0", 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (options.ipv6Address.isNotEmpty()) {
|
if (options.ipv6Address.isNotEmpty()) {
|
||||||
val cidr = options.ipv6Address.toCIDR()
|
val cidr = options.ipv6Address.toCIDR()
|
||||||
addAddress(cidr.address, cidr.prefixLength)
|
addAddress(cidr.address, cidr.prefixLength)
|
||||||
val routeAddress = options.getIpv6RouteAddress()
|
addRoute("::", 0)
|
||||||
if (routeAddress.isNotEmpty()) {
|
|
||||||
routeAddress.forEach { i ->
|
|
||||||
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
|
|
||||||
addRoute(i.address, i.prefixLength)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addRoute("::", 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
addDnsServer(options.dnsServerAddress)
|
addDnsServer(options.dnsServerAddress)
|
||||||
setMtu(9000)
|
setMtu(9000)
|
||||||
@@ -141,6 +122,26 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -151,40 +152,31 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
|||||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||||
}
|
}
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
addAction(
|
|
||||||
0,
|
|
||||||
GlobalState.getText("stop"),
|
|
||||||
getActionPendingIntent("STOP")
|
|
||||||
)
|
|
||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
|
addAction(0, "Stop", stopPendingIntent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
@SuppressLint("ForegroundServiceType", "WrongConstant")
|
||||||
override fun startForeground(title: String, content: String) {
|
override fun startForeground(title: String, content: String) {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
if (channel == null) {
|
||||||
if (channel == null) {
|
channel =
|
||||||
channel =
|
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
manager?.createNotificationChannel(channel)
|
||||||
manager?.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val notification =
|
|
||||||
notificationBuilder
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(content)
|
|
||||||
.build()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
|
||||||
} else {
|
|
||||||
startForeground(notificationId, notification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val notification =
|
||||||
|
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||||
|
} else {
|
||||||
|
startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
|
|||||||
Binary file not shown.
Submodule core/Clash.Meta updated: f7c61f885c...8472840f47
@@ -1,32 +0,0 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (action Action) Json() ([]byte, error) {
|
|
||||||
data, err := json.Marshal(action)
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (action Action) callback(data interface{}) bool {
|
|
||||||
if conn == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sendAction := Action{
|
|
||||||
Id: action.Id,
|
|
||||||
Method: action.Method,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
res, err := sendAction.Json()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err = conn.Write(append(res, []byte("\n")...))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
327
core/common.go
327
core/common.go
@@ -1,10 +1,23 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"core/state"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/metacubex/mihomo/constant/features"
|
||||||
|
"github.com/metacubex/mihomo/hub/route"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter"
|
"github.com/metacubex/mihomo/adapter"
|
||||||
"github.com/metacubex/mihomo/adapter/inbound"
|
"github.com/metacubex/mihomo/adapter/inbound"
|
||||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||||
@@ -14,28 +27,52 @@ import (
|
|||||||
"github.com/metacubex/mihomo/component/resolver"
|
"github.com/metacubex/mihomo/component/resolver"
|
||||||
"github.com/metacubex/mihomo/config"
|
"github.com/metacubex/mihomo/config"
|
||||||
"github.com/metacubex/mihomo/constant"
|
"github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/constant/features"
|
|
||||||
cp "github.com/metacubex/mihomo/constant/provider"
|
cp "github.com/metacubex/mihomo/constant/provider"
|
||||||
"github.com/metacubex/mihomo/hub"
|
"github.com/metacubex/mihomo/hub"
|
||||||
"github.com/metacubex/mihomo/hub/route"
|
"github.com/metacubex/mihomo/hub/executor"
|
||||||
"github.com/metacubex/mihomo/listener"
|
"github.com/metacubex/mihomo/listener"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
rp "github.com/metacubex/mihomo/rules/provider"
|
rp "github.com/metacubex/mihomo/rules/provider"
|
||||||
"github.com/metacubex/mihomo/tunnel"
|
"github.com/metacubex/mihomo/tunnel"
|
||||||
"github.com/samber/lo"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type ConfigExtendedParams struct {
|
||||||
isRunning = false
|
IsPatch bool `json:"is-patch"`
|
||||||
runLock sync.Mutex
|
IsCompatible bool `json:"is-compatible"`
|
||||||
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
|
SelectedMap map[string]string `json:"selected-map"`
|
||||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
TestURL *string `json:"test-url"`
|
||||||
)
|
OverrideDns bool `json:"override-dns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateConfigParams struct {
|
||||||
|
ProfileId string `json:"profile-id"`
|
||||||
|
Config config.RawConfig `json:"config" `
|
||||||
|
Params ConfigExtendedParams `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeProxyParams struct {
|
||||||
|
GroupName *string `json:"group-name"`
|
||||||
|
ProxyName *string `json:"proxy-name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestDelayParams struct {
|
||||||
|
ProxyName string `json:"proxy-name"`
|
||||||
|
Timeout int64 `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessMapItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalProvider struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
VehicleType string `json:"vehicle-type"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
UpdateAt time.Time `json:"update-at"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExternalProviders []ExternalProvider
|
type ExternalProviders []ExternalProvider
|
||||||
|
|
||||||
@@ -43,9 +80,30 @@ func (a ExternalProviders) Len() int { return len(a) }
|
|||||||
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||||
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
func (message *Message) Json() (string, error) {
|
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||||
data, err := json.Marshal(message)
|
|
||||||
return string(data), err
|
func restartExecutable(execPath string) {
|
||||||
|
var err error
|
||||||
|
executor.Shutdown()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd := exec.Command(execPath, os.Args[1:]...)
|
||||||
|
log.Infoln("restarting: %q %q", execPath, os.Args[1:])
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("restarting: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infoln("restarting: %q %q", execPath, os.Args[1:])
|
||||||
|
err = syscall.Exec(execPath, os.Args, os.Environ())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("restarting: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFile(path string) ([]byte, error) {
|
func readFile(path string) ([]byte, error) {
|
||||||
@@ -60,6 +118,19 @@ func readFile(path string) ([]byte, error) {
|
|||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeFile(path string) error {
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.Remove(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getProfilePath(id string) string {
|
func getProfilePath(id string) string {
|
||||||
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
|
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
|
||||||
}
|
}
|
||||||
@@ -87,12 +158,10 @@ func getRawConfigWithId(id string) *config.RawConfig {
|
|||||||
}
|
}
|
||||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||||
if configParams.TestURL != nil {
|
if configParams.TestURL != nil {
|
||||||
if mapping["health-check"] != nil {
|
hc := mapping["health-check"].(map[string]any)
|
||||||
hc := mapping["health-check"].(map[string]any)
|
if hc != nil {
|
||||||
if hc != nil {
|
if hc["url"] != nil {
|
||||||
if hc["url"] != nil {
|
hc["url"] = *configParams.TestURL
|
||||||
hc["url"] = *configParams.TestURL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,13 +196,12 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
|
|||||||
case *provider.ProxySetProvider:
|
case *provider.ProxySetProvider:
|
||||||
psp := p.(*provider.ProxySetProvider)
|
psp := p.(*provider.ProxySetProvider)
|
||||||
return &ExternalProvider{
|
return &ExternalProvider{
|
||||||
Name: psp.Name(),
|
Name: psp.Name(),
|
||||||
Type: psp.Type().String(),
|
Type: psp.Type().String(),
|
||||||
VehicleType: psp.VehicleType().String(),
|
VehicleType: psp.VehicleType().String(),
|
||||||
Count: psp.Count(),
|
Count: psp.Count(),
|
||||||
UpdateAt: psp.UpdatedAt(),
|
Path: psp.Vehicle().Path(),
|
||||||
Path: psp.Vehicle().Path(),
|
UpdateAt: psp.UpdatedAt(),
|
||||||
SubscriptionInfo: psp.GetSubscriptionInfo(),
|
|
||||||
}, nil
|
}, nil
|
||||||
case *rp.RuleSetProvider:
|
case *rp.RuleSetProvider:
|
||||||
rsp := p.(*rp.RuleSetProvider)
|
rsp := p.(*rp.RuleSetProvider)
|
||||||
@@ -142,8 +210,8 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
|
|||||||
Type: rsp.Type().String(),
|
Type: rsp.Type().String(),
|
||||||
VehicleType: rsp.VehicleType().String(),
|
VehicleType: rsp.VehicleType().String(),
|
||||||
Count: rsp.Count(),
|
Count: rsp.Count(),
|
||||||
UpdateAt: rsp.UpdatedAt(),
|
|
||||||
Path: rsp.Vehicle().Path(),
|
Path: rsp.Vehicle().Path(),
|
||||||
|
UpdateAt: rsp.UpdatedAt(),
|
||||||
}, nil
|
}, nil
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("not external provider")
|
return nil, errors.New("not external provider")
|
||||||
@@ -177,6 +245,152 @@ 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 {
|
||||||
|
// for _, v := range s {
|
||||||
|
// initVal = f(initVal, v)
|
||||||
|
// }
|
||||||
|
// return initVal
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func Map[T, U any](slice []T, fn func(T) U) []U {
|
||||||
|
// result := make([]U, len(slice))
|
||||||
|
// for i, v := range slice {
|
||||||
|
// result[i] = fn(v)
|
||||||
|
// }
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func replaceFromMap(s string, m map[string]string) string {
|
||||||
|
// for k, v := range m {
|
||||||
|
// s = strings.ReplaceAll(s, k, v)
|
||||||
|
// }
|
||||||
|
// return s
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func removeDuplicateFromSlice[T any](slice []T) []T {
|
||||||
|
// result := make([]T, 0)
|
||||||
|
// seen := make(map[any]struct{})
|
||||||
|
// for _, value := range slice {
|
||||||
|
// if _, ok := seen[value]; !ok {
|
||||||
|
// result = append(result, value)
|
||||||
|
// seen[value] = struct{}{}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return result
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
|
||||||
|
// var replacements = map[string]string{}
|
||||||
|
// var selectArr []map[string]any
|
||||||
|
// var urlTestArr []map[string]any
|
||||||
|
// var fallbackArr []map[string]any
|
||||||
|
// for _, group := range *proxyGroup {
|
||||||
|
// switch group["type"] {
|
||||||
|
// case "select":
|
||||||
|
// selectArr = append(selectArr, group)
|
||||||
|
// replacements[group["name"].(string)] = "Proxy"
|
||||||
|
// break
|
||||||
|
// case "url-test":
|
||||||
|
// urlTestArr = append(urlTestArr, group)
|
||||||
|
// replacements[group["name"].(string)] = "Auto"
|
||||||
|
// break
|
||||||
|
// case "fallback":
|
||||||
|
// fallbackArr = append(fallbackArr, group)
|
||||||
|
// replacements[group["name"].(string)] = "Fallback"
|
||||||
|
// break
|
||||||
|
// default:
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||||
|
// if cur["proxies"] == nil {
|
||||||
|
// return res
|
||||||
|
// }
|
||||||
|
// for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||||
|
// if str, ok := proxyName.(string); ok {
|
||||||
|
// str = replaceFromMap(str, replacements)
|
||||||
|
// if str != "Proxy" {
|
||||||
|
// res = append(res, str)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return res
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
|
||||||
|
//
|
||||||
|
// AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||||
|
// if cur["proxies"] == nil {
|
||||||
|
// return res
|
||||||
|
// }
|
||||||
|
// for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||||
|
// if str, ok := proxyName.(string); ok {
|
||||||
|
// str = replaceFromMap(str, replacements)
|
||||||
|
// if str != "Auto" {
|
||||||
|
// res = append(res, str)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return res
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// AutoProxies = removeDuplicateFromSlice(AutoProxies)
|
||||||
|
//
|
||||||
|
// FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||||
|
// if cur["proxies"] == nil {
|
||||||
|
// return res
|
||||||
|
// }
|
||||||
|
// for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||||
|
// if str, ok := proxyName.(string); ok {
|
||||||
|
// str = replaceFromMap(str, replacements)
|
||||||
|
// if str != "Fallback" {
|
||||||
|
// res = append(res, str)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return res
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
|
||||||
|
//
|
||||||
|
// var computedProxyGroup []map[string]any
|
||||||
|
//
|
||||||
|
// if len(ProxyProxies) > 0 {
|
||||||
|
// computedProxyGroup = append(computedProxyGroup,
|
||||||
|
// map[string]any{
|
||||||
|
// "name": "Proxy",
|
||||||
|
// "type": "select",
|
||||||
|
// "proxies": ProxyProxies,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if len(AutoProxies) > 0 {
|
||||||
|
// computedProxyGroup = append(computedProxyGroup,
|
||||||
|
// map[string]any{
|
||||||
|
// "name": "Auto",
|
||||||
|
// "type": "url-test",
|
||||||
|
// "proxies": AutoProxies,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if len(FallbackProxies) > 0 {
|
||||||
|
// computedProxyGroup = append(computedProxyGroup,
|
||||||
|
// map[string]any{
|
||||||
|
// "name": "Fallback",
|
||||||
|
// "type": "fallback",
|
||||||
|
// "proxies": FallbackProxies,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// computedRule := Map(*rule, func(value string) string {
|
||||||
|
// return replaceFromMap(value, replacements)
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// *proxyGroup = computedProxyGroup
|
||||||
|
// *rule = computedRule
|
||||||
|
//}
|
||||||
|
|
||||||
func genHosts(hosts, patchHosts map[string]any) {
|
func genHosts(hosts, patchHosts map[string]any) {
|
||||||
for k, v := range patchHosts {
|
for k, v := range patchHosts {
|
||||||
hosts[k] = v
|
hosts[k] = v
|
||||||
@@ -190,6 +404,8 @@ func trimArr(arr []string) (r []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
|
||||||
|
|
||||||
func overrideRules(rules *[]string) {
|
func overrideRules(rules *[]string) {
|
||||||
var target = ""
|
var target = ""
|
||||||
for _, line := range *rules {
|
for _, line := range *rules {
|
||||||
@@ -251,13 +467,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
overrideRules(&targetConfig.Rule)
|
overrideRules(&targetConfig.Rule)
|
||||||
|
//if runtime.GOOS == "android" {
|
||||||
|
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
|
||||||
|
//} else if runtime.GOOS == "windows" {
|
||||||
|
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
|
||||||
|
//}
|
||||||
|
//if configParams.IsCompatible == false {
|
||||||
|
// targetConfig.ProxyProvider = make(map[string]map[string]any)
|
||||||
|
// targetConfig.RuleProvider = make(map[string]map[string]any)
|
||||||
|
// generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
func patchConfig() {
|
func patchConfig(general *config.General, controller *config.Controller, tls *config.TLS) {
|
||||||
log.Infoln("[Apply] patch")
|
log.Infoln("[Apply] patch")
|
||||||
general := currentConfig.General
|
|
||||||
controller := currentConfig.Controller
|
|
||||||
tls := currentConfig.TLS
|
|
||||||
tunnel.SetSniffing(general.Sniffing)
|
tunnel.SetSniffing(general.Sniffing)
|
||||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||||
@@ -284,15 +507,17 @@ func patchConfig() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateListeners(force bool) {
|
var isRunning = false
|
||||||
|
|
||||||
|
var runLock sync.Mutex
|
||||||
|
|
||||||
|
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
|
||||||
if !isRunning {
|
if !isRunning {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
general := currentConfig.General
|
runLock.Lock()
|
||||||
listeners := currentConfig.Listeners
|
defer runLock.Unlock()
|
||||||
if force == true {
|
stopListeners()
|
||||||
stopListeners()
|
|
||||||
}
|
|
||||||
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)
|
||||||
@@ -341,22 +566,20 @@ func patchSelectGroup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyConfig(rawConfig *config.RawConfig) error {
|
func applyConfig() error {
|
||||||
runLock.Lock()
|
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
|
||||||
defer runLock.Unlock()
|
|
||||||
var err error
|
|
||||||
currentConfig, err = config.ParseRawConfig(rawConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||||
}
|
}
|
||||||
if configParams.IsPatch {
|
if configParams.IsPatch {
|
||||||
patchConfig()
|
patchConfig(cfg.General, cfg.Controller, cfg.TLS)
|
||||||
} else {
|
} else {
|
||||||
handleCloseConnectionsUnLock()
|
closeConnections()
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
hub.ApplyConfig(currentConfig)
|
hub.ApplyConfig(cfg)
|
||||||
patchSelectGroup()
|
patchSelectGroup()
|
||||||
}
|
}
|
||||||
updateListeners(false)
|
updateListeners(cfg.General, cfg.Listeners)
|
||||||
|
externalProviders = getExternalProvidersRaw()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
112
core/constant.go
112
core/constant.go
@@ -1,112 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/metacubex/mihomo/adapter/provider"
|
|
||||||
"github.com/metacubex/mihomo/config"
|
|
||||||
"github.com/metacubex/mihomo/constant"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConfigExtendedParams struct {
|
|
||||||
IsPatch bool `json:"is-patch"`
|
|
||||||
IsCompatible bool `json:"is-compatible"`
|
|
||||||
SelectedMap map[string]string `json:"selected-map"`
|
|
||||||
TestURL *string `json:"test-url"`
|
|
||||||
OverrideDns bool `json:"override-dns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenerateConfigParams struct {
|
|
||||||
ProfileId string `json:"profile-id"`
|
|
||||||
Config config.RawConfig `json:"config" `
|
|
||||||
Params ConfigExtendedParams `json:"params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeProxyParams struct {
|
|
||||||
GroupName *string `json:"group-name"`
|
|
||||||
ProxyName *string `json:"proxy-name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestDelayParams struct {
|
|
||||||
ProxyName string `json:"proxy-name"`
|
|
||||||
Timeout int64 `json:"timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProcessMapItem struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExternalProvider struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
VehicleType string `json:"vehicle-type"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
UpdateAt time.Time `json:"update-at"`
|
|
||||||
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
messageMethod Method = "message"
|
|
||||||
initClashMethod Method = "initClash"
|
|
||||||
getIsInitMethod Method = "getIsInit"
|
|
||||||
forceGcMethod Method = "forceGc"
|
|
||||||
shutdownMethod Method = "shutdown"
|
|
||||||
validateConfigMethod Method = "validateConfig"
|
|
||||||
updateConfigMethod Method = "updateConfig"
|
|
||||||
getProxiesMethod Method = "getProxies"
|
|
||||||
changeProxyMethod Method = "changeProxy"
|
|
||||||
getTrafficMethod Method = "getTraffic"
|
|
||||||
getTotalTrafficMethod Method = "getTotalTraffic"
|
|
||||||
resetTrafficMethod Method = "resetTraffic"
|
|
||||||
asyncTestDelayMethod Method = "asyncTestDelay"
|
|
||||||
getConnectionsMethod Method = "getConnections"
|
|
||||||
closeConnectionsMethod Method = "closeConnections"
|
|
||||||
closeConnectionMethod Method = "closeConnection"
|
|
||||||
getExternalProvidersMethod Method = "getExternalProviders"
|
|
||||||
getExternalProviderMethod Method = "getExternalProvider"
|
|
||||||
getCountryCodeMethod Method = "getCountryCode"
|
|
||||||
getMemoryMethod Method = "getMemory"
|
|
||||||
updateGeoDataMethod Method = "updateGeoData"
|
|
||||||
updateExternalProviderMethod Method = "updateExternalProvider"
|
|
||||||
sideLoadExternalProviderMethod Method = "sideLoadExternalProvider"
|
|
||||||
startLogMethod Method = "startLog"
|
|
||||||
stopLogMethod Method = "stopLog"
|
|
||||||
startListenerMethod Method = "startListener"
|
|
||||||
stopListenerMethod Method = "stopListener"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Method string
|
|
||||||
|
|
||||||
type Action struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Method Method `json:"method"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageType string
|
|
||||||
|
|
||||||
type Delay struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value int32 `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
Type MessageType `json:"type"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Process struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Metadata *constant.Metadata `json:"metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
LogMessage MessageType = "log"
|
|
||||||
ProtectMessage MessageType = "protect"
|
|
||||||
DelayMessage MessageType = "delay"
|
|
||||||
ProcessMessage MessageType = "process"
|
|
||||||
RequestMessage MessageType = "request"
|
|
||||||
StartedMessage MessageType = "started"
|
|
||||||
LoadedMessage MessageType = "loaded"
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build cgo
|
|
||||||
|
|
||||||
package dart_bridge
|
package dart_bridge
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package dart_bridge
|
|
||||||
|
|
||||||
func SendToPort(port int64, msg string) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
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()
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module core
|
module core
|
||||||
|
|
||||||
go 1.21
|
go 1.21.0
|
||||||
|
|
||||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||||
|
|
||||||
|
|||||||
487
core/hub.go
487
core/hub.go
@@ -1,74 +1,82 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <stdlib.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
bridge "core/dart-bridge"
|
||||||
|
"core/state"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter"
|
"github.com/metacubex/mihomo/adapter"
|
||||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||||
"github.com/metacubex/mihomo/common/observable"
|
"github.com/metacubex/mihomo/adapter/provider"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
|
||||||
"github.com/metacubex/mihomo/component/mmdb"
|
|
||||||
"github.com/metacubex/mihomo/component/updater"
|
"github.com/metacubex/mihomo/component/updater"
|
||||||
"github.com/metacubex/mihomo/config"
|
"github.com/metacubex/mihomo/config"
|
||||||
"github.com/metacubex/mihomo/constant"
|
"github.com/metacubex/mihomo/constant"
|
||||||
cp "github.com/metacubex/mihomo/constant/provider"
|
cp "github.com/metacubex/mihomo/constant/provider"
|
||||||
"github.com/metacubex/mihomo/hub/executor"
|
"github.com/metacubex/mihomo/hub/executor"
|
||||||
"github.com/metacubex/mihomo/listener"
|
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
"github.com/metacubex/mihomo/tunnel"
|
"github.com/metacubex/mihomo/tunnel"
|
||||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||||
"net"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var configParams = ConfigExtendedParams{}
|
||||||
isInit = false
|
|
||||||
configParams = ConfigExtendedParams{}
|
|
||||||
externalProviders = map[string]cp.Provider{}
|
|
||||||
logSubscriber observable.Subscription[log.Event]
|
|
||||||
currentConfig *config.Config
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleInitClash(homeDirStr string) bool {
|
var externalProviders = map[string]cp.Provider{}
|
||||||
|
|
||||||
|
var isInit = false
|
||||||
|
|
||||||
|
//export start
|
||||||
|
func start() {
|
||||||
|
runLock.Lock()
|
||||||
|
defer runLock.Unlock()
|
||||||
|
isRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
//export stop
|
||||||
|
func stop() {
|
||||||
|
runLock.Lock()
|
||||||
|
go func() {
|
||||||
|
defer runLock.Unlock()
|
||||||
|
isRunning = false
|
||||||
|
stopListeners()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export initClash
|
||||||
|
func initClash(homeDirStr *C.char) bool {
|
||||||
if !isInit {
|
if !isInit {
|
||||||
constant.SetHomeDir(homeDirStr)
|
constant.SetHomeDir(C.GoString(homeDirStr))
|
||||||
isInit = true
|
isInit = true
|
||||||
}
|
}
|
||||||
return isInit
|
return isInit
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStartListener() bool {
|
//export getIsInit
|
||||||
runLock.Lock()
|
func getIsInit() bool {
|
||||||
defer runLock.Unlock()
|
|
||||||
isRunning = true
|
|
||||||
updateListeners(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStopListener() bool {
|
|
||||||
runLock.Lock()
|
|
||||||
defer runLock.Unlock()
|
|
||||||
isRunning = false
|
|
||||||
listener.StopListener()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetIsInit() bool {
|
|
||||||
return isInit
|
return isInit
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleForceGc() {
|
//export restartClash
|
||||||
go func() {
|
func restartClash() bool {
|
||||||
log.Infoln("[APP] request force GC")
|
execPath, _ := os.Executable()
|
||||||
runtime.GC()
|
go restartExecutable(execPath)
|
||||||
}()
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleShutdown() bool {
|
//export shutdownClash
|
||||||
|
func shutdownClash() bool {
|
||||||
stopListeners()
|
stopListeners()
|
||||||
executor.Shutdown()
|
executor.Shutdown()
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
@@ -76,81 +84,106 @@ func handleShutdown() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleValidateConfig(bytes []byte) string {
|
//export forceGc
|
||||||
_, err := config.UnmarshalRawConfig(bytes)
|
func forceGc() {
|
||||||
if err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleUpdateConfig(bytes []byte) string {
|
|
||||||
var params = &GenerateConfigParams{}
|
|
||||||
err := json.Unmarshal(bytes, params)
|
|
||||||
if err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
configParams = params.Params
|
|
||||||
prof := decorationConfig(params.ProfileId, params.Config)
|
|
||||||
err = applyConfig(prof)
|
|
||||||
if err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetProxies() string {
|
|
||||||
runLock.Lock()
|
|
||||||
defer runLock.Unlock()
|
|
||||||
data, err := json.Marshal(tunnel.ProxiesWithProviders())
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleChangeProxy(data string, fn func(string string)) {
|
|
||||||
runLock.Lock()
|
|
||||||
go func() {
|
go func() {
|
||||||
defer runLock.Unlock()
|
log.Infoln("[APP] request force GC")
|
||||||
var params = &ChangeProxyParams{}
|
runtime.GC()
|
||||||
err := json.Unmarshal([]byte(data), params)
|
|
||||||
if err != nil {
|
|
||||||
fn(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
groupName := *params.GroupName
|
|
||||||
proxyName := *params.ProxyName
|
|
||||||
proxies := tunnel.ProxiesWithProviders()
|
|
||||||
group, ok := proxies[groupName]
|
|
||||||
if !ok {
|
|
||||||
fn("Not found group")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
adapterProxy := group.(*adapter.Proxy)
|
|
||||||
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
|
|
||||||
if !ok {
|
|
||||||
fn("Group is not selectable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if proxyName == "" {
|
|
||||||
selector.ForceSet(proxyName)
|
|
||||||
} else {
|
|
||||||
err = selector.Set(proxyName)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
fn(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fn("")
|
|
||||||
return
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetTraffic(onlyProxy bool) string {
|
//export validateConfig
|
||||||
up, down := statistic.DefaultManager.Current(onlyProxy)
|
func validateConfig(s *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
bytes := []byte(C.GoString(s))
|
||||||
|
go func() {
|
||||||
|
_, err := config.UnmarshalRawConfig(bytes)
|
||||||
|
if err != nil {
|
||||||
|
bridge.SendToPort(i, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bridge.SendToPort(i, "")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateLock sync.Mutex
|
||||||
|
|
||||||
|
//export updateConfig
|
||||||
|
func updateConfig(s *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
paramsString := C.GoString(s)
|
||||||
|
go func() {
|
||||||
|
updateLock.Lock()
|
||||||
|
defer updateLock.Unlock()
|
||||||
|
var params = &GenerateConfigParams{}
|
||||||
|
err := json.Unmarshal([]byte(paramsString), params)
|
||||||
|
if err != nil {
|
||||||
|
bridge.SendToPort(i, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configParams = params.Params
|
||||||
|
prof := decorationConfig(params.ProfileId, params.Config)
|
||||||
|
state.CurrentRawConfig = prof
|
||||||
|
err = applyConfig()
|
||||||
|
if err != nil {
|
||||||
|
bridge.SendToPort(i, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bridge.SendToPort(i, "")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export clearEffect
|
||||||
|
func clearEffect(s *C.char) {
|
||||||
|
id := C.GoString(s)
|
||||||
|
go func() {
|
||||||
|
_ = removeFile(getProfilePath(id))
|
||||||
|
_ = removeFile(getProfileProvidersPath(id))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getProxies
|
||||||
|
func getProxies() *C.char {
|
||||||
|
data, err := json.Marshal(tunnel.ProxiesWithProviders())
|
||||||
|
if err != nil {
|
||||||
|
return C.CString("")
|
||||||
|
}
|
||||||
|
return C.CString(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export changeProxy
|
||||||
|
func changeProxy(s *C.char) {
|
||||||
|
paramsString := C.GoString(s)
|
||||||
|
var params = &ChangeProxyParams{}
|
||||||
|
err := json.Unmarshal([]byte(paramsString), params)
|
||||||
|
if err != nil {
|
||||||
|
log.Infoln("Unmarshal ChangeProxyParams %v", err)
|
||||||
|
}
|
||||||
|
groupName := *params.GroupName
|
||||||
|
proxyName := *params.ProxyName
|
||||||
|
proxies := tunnel.ProxiesWithProviders()
|
||||||
|
group, ok := proxies[groupName]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adapterProxy := group.(*adapter.Proxy)
|
||||||
|
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if proxyName == "" {
|
||||||
|
selector.ForceSet(proxyName)
|
||||||
|
} else {
|
||||||
|
err = selector.Set(proxyName)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getTraffic
|
||||||
|
func getTraffic() *C.char {
|
||||||
|
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
|
||||||
traffic := map[string]int64{
|
traffic := map[string]int64{
|
||||||
"up": up,
|
"up": up,
|
||||||
"down": down,
|
"down": down,
|
||||||
@@ -158,13 +191,14 @@ func handleGetTraffic(onlyProxy bool) string {
|
|||||||
data, err := json.Marshal(traffic)
|
data, err := json.Marshal(traffic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error:", err)
|
fmt.Println("Error:", err)
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
return string(data)
|
return C.CString(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetTotalTraffic(onlyProxy bool) string {
|
//export getTotalTraffic
|
||||||
up, down := statistic.DefaultManager.Total(onlyProxy)
|
func getTotalTraffic() *C.char {
|
||||||
|
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
|
||||||
traffic := map[string]int64{
|
traffic := map[string]int64{
|
||||||
"up": up,
|
"up": up,
|
||||||
"down": down,
|
"down": down,
|
||||||
@@ -172,27 +206,31 @@ func handleGetTotalTraffic(onlyProxy bool) string {
|
|||||||
data, err := json.Marshal(traffic)
|
data, err := json.Marshal(traffic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error:", err)
|
fmt.Println("Error:", err)
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
return string(data)
|
return C.CString(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleResetTraffic() {
|
//export resetTraffic
|
||||||
|
func resetTraffic() {
|
||||||
statistic.DefaultManager.ResetStatistic()
|
statistic.DefaultManager.ResetStatistic()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
//export asyncTestDelay
|
||||||
|
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
paramsString := C.GoString(s)
|
||||||
b.Go(paramsString, func() (bool, error) {
|
b.Go(paramsString, func() (bool, error) {
|
||||||
var params = &TestDelayParams{}
|
var params = &TestDelayParams{}
|
||||||
err := json.Unmarshal([]byte(paramsString), params)
|
err := json.Unmarshal([]byte(paramsString), params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn("")
|
bridge.SendToPort(i, "")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
|
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn("")
|
bridge.SendToPort(i, "")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +247,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
|||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
delayData.Value = -1
|
delayData.Value = -1
|
||||||
data, _ := json.Marshal(delayData)
|
data, _ := json.Marshal(delayData)
|
||||||
fn(string(data))
|
bridge.SendToPort(i, string(data))
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,30 +255,44 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
|||||||
if err != nil || delay == 0 {
|
if err != nil || delay == 0 {
|
||||||
delayData.Value = -1
|
delayData.Value = -1
|
||||||
data, _ := json.Marshal(delayData)
|
data, _ := json.Marshal(delayData)
|
||||||
fn(string(data))
|
bridge.SendToPort(i, string(data))
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
delayData.Value = int32(delay)
|
delayData.Value = int32(delay)
|
||||||
data, _ := json.Marshal(delayData)
|
data, _ := json.Marshal(delayData)
|
||||||
fn(string(data))
|
bridge.SendToPort(i, string(data))
|
||||||
return false, nil
|
return false, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetConnections() string {
|
//export getVersionInfo
|
||||||
runLock.Lock()
|
func getVersionInfo() *C.char {
|
||||||
defer runLock.Unlock()
|
versionInfo := map[string]string{
|
||||||
|
"clashName": constant.Name,
|
||||||
|
"version": "1.18.5",
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(versionInfo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return C.CString("")
|
||||||
|
}
|
||||||
|
return C.CString(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getConnections
|
||||||
|
func getConnections() *C.char {
|
||||||
snapshot := statistic.DefaultManager.Snapshot()
|
snapshot := statistic.DefaultManager.Snapshot()
|
||||||
data, err := json.Marshal(snapshot)
|
data, err := json.Marshal(snapshot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error:", err)
|
fmt.Println("Error:", err)
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
return string(data)
|
return C.CString(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseConnectionsUnLock() bool {
|
//export closeConnections
|
||||||
|
func closeConnections() {
|
||||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||||
err := c.Close()
|
err := c.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -248,37 +300,43 @@ func handleCloseConnectionsUnLock() bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseConnections() bool {
|
//export closeConnection
|
||||||
runLock.Lock()
|
func closeConnection(id *C.char) {
|
||||||
defer runLock.Unlock()
|
connectionId := C.GoString(id)
|
||||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
|
||||||
err := c.Close()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCloseConnection(connectionId string) bool {
|
|
||||||
runLock.Lock()
|
|
||||||
defer runLock.Unlock()
|
|
||||||
c := statistic.DefaultManager.Get(connectionId)
|
c := statistic.DefaultManager.Get(connectionId)
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetExternalProviders() string {
|
//export getProviders
|
||||||
runLock.Lock()
|
func getProviders() *C.char {
|
||||||
defer runLock.Unlock()
|
data, err := json.Marshal(tunnel.Providers())
|
||||||
externalProviders = getExternalProvidersRaw()
|
var msg *C.char
|
||||||
|
if err != nil {
|
||||||
|
msg = C.CString("")
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
msg = C.CString(string(data))
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getProvider
|
||||||
|
func getProvider(name *C.char) *C.char {
|
||||||
|
providerName := C.GoString(name)
|
||||||
|
providers := tunnel.Providers()
|
||||||
|
data, err := json.Marshal(providers[providerName])
|
||||||
|
if err != nil {
|
||||||
|
return C.CString("")
|
||||||
|
}
|
||||||
|
return C.CString(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getExternalProviders
|
||||||
|
func getExternalProviders() *C.char {
|
||||||
eps := make([]ExternalProvider, 0)
|
eps := make([]ExternalProvider, 0)
|
||||||
for _, p := range externalProviders {
|
for _, p := range externalProviders {
|
||||||
externalProvider, err := toExternalProvider(p)
|
externalProvider, err := toExternalProvider(p)
|
||||||
@@ -290,144 +348,123 @@ func handleGetExternalProviders() string {
|
|||||||
sort.Sort(ExternalProviders(eps))
|
sort.Sort(ExternalProviders(eps))
|
||||||
data, err := json.Marshal(eps)
|
data, err := json.Marshal(eps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
return string(data)
|
return C.CString(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetExternalProvider(externalProviderName string) string {
|
//export getExternalProvider
|
||||||
runLock.Lock()
|
func getExternalProvider(name *C.char) *C.char {
|
||||||
defer runLock.Unlock()
|
externalProviderName := C.GoString(name)
|
||||||
externalProvider, exist := externalProviders[externalProviderName]
|
externalProvider, exist := externalProviders[externalProviderName]
|
||||||
if !exist {
|
if !exist {
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
e, err := toExternalProvider(externalProvider)
|
e, err := toExternalProvider(externalProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(e)
|
data, err := json.Marshal(e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return C.CString("")
|
||||||
}
|
}
|
||||||
return string(data)
|
return C.CString(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateGeoData(geoType string, geoName string, fn func(value string)) {
|
//export updateGeoData
|
||||||
|
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
geoTypeString := C.GoString(geoType)
|
||||||
|
geoNameString := C.GoString(geoName)
|
||||||
go func() {
|
go func() {
|
||||||
path := constant.Path.Resolve(geoName)
|
path := constant.Path.Resolve(geoNameString)
|
||||||
switch geoType {
|
switch geoTypeString {
|
||||||
case "MMDB":
|
case "MMDB":
|
||||||
err := updater.UpdateMMDBWithPath(path)
|
err := updater.UpdateMMDBWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn(err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "ASN":
|
case "ASN":
|
||||||
err := updater.UpdateASNWithPath(path)
|
err := updater.UpdateASNWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn(err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "GeoIp":
|
case "GeoIp":
|
||||||
err := updater.UpdateGeoIpWithPath(path)
|
err := updater.UpdateGeoIpWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn(err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "GeoSite":
|
case "GeoSite":
|
||||||
err := updater.UpdateGeoSiteWithPath(path)
|
err := updater.UpdateGeoSiteWithPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn(err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn("")
|
bridge.SendToPort(i, "")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateExternalProvider(providerName string, fn func(value string)) {
|
//export updateExternalProvider
|
||||||
|
func updateExternalProvider(providerName *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
providerNameString := C.GoString(providerName)
|
||||||
go func() {
|
go func() {
|
||||||
externalProvider, exist := externalProviders[providerName]
|
externalProvider, exist := externalProviders[providerNameString]
|
||||||
if !exist {
|
if !exist {
|
||||||
fn("external provider is not exist")
|
bridge.SendToPort(i, "external provider is not exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := externalProvider.Update()
|
err := externalProvider.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn(err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fn("")
|
bridge.SendToPort(i, "")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSideLoadExternalProvider(providerName string, data []byte, fn func(value string)) {
|
//export sideLoadExternalProvider
|
||||||
|
func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
bytes := []byte(C.GoString(data))
|
||||||
|
providerNameString := C.GoString(providerName)
|
||||||
go func() {
|
go func() {
|
||||||
runLock.Lock()
|
externalProvider, exist := externalProviders[providerNameString]
|
||||||
defer runLock.Unlock()
|
|
||||||
externalProvider, exist := externalProviders[providerName]
|
|
||||||
if !exist {
|
if !exist {
|
||||||
fn("external provider is not exist")
|
bridge.SendToPort(i, "external provider is not exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := sideUpdateExternalProvider(externalProvider, data)
|
err := sideUpdateExternalProvider(externalProvider, bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fn(err.Error())
|
bridge.SendToPort(i, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fn("")
|
bridge.SendToPort(i, "")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStartLog() {
|
//export initNativeApiBridge
|
||||||
if logSubscriber != nil {
|
func initNativeApiBridge(api unsafe.Pointer) {
|
||||||
log.UnSubscribe(logSubscriber)
|
bridge.InitDartApi(api)
|
||||||
logSubscriber = nil
|
|
||||||
}
|
|
||||||
logSubscriber = log.Subscribe()
|
|
||||||
go func() {
|
|
||||||
for logData := range logSubscriber {
|
|
||||||
if logData.LogLevel < log.Level() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
message := &Message{
|
|
||||||
Type: LogMessage,
|
|
||||||
Data: logData,
|
|
||||||
}
|
|
||||||
SendMessage(*message)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStopLog() {
|
//export initMessage
|
||||||
if logSubscriber != nil {
|
func initMessage(port C.longlong) {
|
||||||
log.UnSubscribe(logSubscriber)
|
i := int64(port)
|
||||||
logSubscriber = nil
|
Port = i
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetCountryCode(ip string, fn func(value string)) {
|
//export freeCString
|
||||||
go func() {
|
func freeCString(s *C.char) {
|
||||||
runLock.Lock()
|
C.free(unsafe.Pointer(s))
|
||||||
defer runLock.Unlock()
|
|
||||||
codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip))
|
|
||||||
if len(codes) == 0 {
|
|
||||||
fn("")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fn(codes[0])
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetMemory(fn func(value string)) {
|
|
||||||
go func() {
|
|
||||||
fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10))
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
adapter.UrlTestHook = func(name string, delay uint16) {
|
provider.HealthcheckHook = func(name string, delay uint16) {
|
||||||
delayData := &Delay{
|
delayData := &Delay{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
|
|||||||
199
core/lib.go
199
core/lib.go
@@ -1,199 +0,0 @@
|
|||||||
//go:build cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
/*
|
|
||||||
#include <stdlib.h>
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
bridge "core/dart-bridge"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
//export initNativeApiBridge
|
|
||||||
func initNativeApiBridge(api unsafe.Pointer) {
|
|
||||||
bridge.InitDartApi(api)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export initMessage
|
|
||||||
func initMessage(port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
Port = i
|
|
||||||
}
|
|
||||||
|
|
||||||
//export freeCString
|
|
||||||
func freeCString(s *C.char) {
|
|
||||||
C.free(unsafe.Pointer(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export initClash
|
|
||||||
func initClash(homeDirStr *C.char) bool {
|
|
||||||
return handleInitClash(C.GoString(homeDirStr))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export startListener
|
|
||||||
func startListener() {
|
|
||||||
handleStartListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export stopListener
|
|
||||||
func stopListener() {
|
|
||||||
handleStopListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getIsInit
|
|
||||||
func getIsInit() bool {
|
|
||||||
return handleGetIsInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export shutdownClash
|
|
||||||
func shutdownClash() bool {
|
|
||||||
return handleShutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export forceGc
|
|
||||||
func forceGc() {
|
|
||||||
handleForceGc()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export validateConfig
|
|
||||||
func validateConfig(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
bytes := []byte(C.GoString(s))
|
|
||||||
go func() {
|
|
||||||
bridge.SendToPort(i, handleValidateConfig(bytes))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export updateConfig
|
|
||||||
func updateConfig(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
bytes := []byte(C.GoString(s))
|
|
||||||
go func() {
|
|
||||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getProxies
|
|
||||||
func getProxies() *C.char {
|
|
||||||
return C.CString(handleGetProxies())
|
|
||||||
}
|
|
||||||
|
|
||||||
//export changeProxy
|
|
||||||
func changeProxy(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
handleChangeProxy(paramsString, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getTraffic
|
|
||||||
func getTraffic(port C.int) *C.char {
|
|
||||||
onlyProxy := int(port) == 1
|
|
||||||
return C.CString(handleGetTraffic(onlyProxy))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getTotalTraffic
|
|
||||||
func getTotalTraffic(port C.int) *C.char {
|
|
||||||
onlyProxy := int(port) == 1
|
|
||||||
return C.CString(handleGetTotalTraffic(onlyProxy))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export resetTraffic
|
|
||||||
func resetTraffic() {
|
|
||||||
handleResetTraffic()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export asyncTestDelay
|
|
||||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
handleAsyncTestDelay(paramsString, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getConnections
|
|
||||||
func getConnections() *C.char {
|
|
||||||
return C.CString(handleGetConnections())
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getMemory
|
|
||||||
func getMemory(port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
handleGetMemory(func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export closeConnections
|
|
||||||
func closeConnections() {
|
|
||||||
handleCloseConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export closeConnection
|
|
||||||
func closeConnection(id *C.char) {
|
|
||||||
connectionId := C.GoString(id)
|
|
||||||
handleCloseConnection(connectionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getExternalProviders
|
|
||||||
func getExternalProviders() *C.char {
|
|
||||||
return C.CString(handleGetExternalProviders())
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getExternalProvider
|
|
||||||
func getExternalProvider(externalProviderNameChar *C.char) *C.char {
|
|
||||||
externalProviderName := C.GoString(externalProviderNameChar)
|
|
||||||
return C.CString(handleGetExternalProvider(externalProviderName))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export updateGeoData
|
|
||||||
func updateGeoData(geoTypeChar *C.char, geoNameChar *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
geoType := C.GoString(geoTypeChar)
|
|
||||||
geoName := C.GoString(geoNameChar)
|
|
||||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export updateExternalProvider
|
|
||||||
func updateExternalProvider(providerNameChar *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
providerName := C.GoString(providerNameChar)
|
|
||||||
handleUpdateExternalProvider(providerName, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getCountryCode
|
|
||||||
func getCountryCode(ipChar *C.char, port C.longlong) {
|
|
||||||
ip := C.GoString(ipChar)
|
|
||||||
i := int64(port)
|
|
||||||
handleGetCountryCode(ip, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export sideLoadExternalProvider
|
|
||||||
func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
providerName := C.GoString(providerNameChar)
|
|
||||||
data := []byte(C.GoString(dataChar))
|
|
||||||
handleSideLoadExternalProvider(providerName, data, func(value string) {
|
|
||||||
bridge.SendToPort(i, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export startLog
|
|
||||||
func startLog() {
|
|
||||||
handleStartLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export stopLog
|
|
||||||
func stopLog() {
|
|
||||||
handleStopLog()
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
//go:build android && cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"core/platform"
|
|
||||||
"core/state"
|
|
||||||
t "core/tun"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/metacubex/mihomo/component/dialer"
|
|
||||||
"github.com/metacubex/mihomo/component/process"
|
|
||||||
"github.com/metacubex/mihomo/constant"
|
|
||||||
"github.com/metacubex/mihomo/dns"
|
|
||||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
|
||||||
"github.com/metacubex/mihomo/log"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProcessMap struct {
|
|
||||||
m sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
type FdMap struct {
|
|
||||||
m sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
type Fd struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Value int64 `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
tunListener *sing_tun.Listener
|
|
||||||
fdMap FdMap
|
|
||||||
fdCounter int64 = 0
|
|
||||||
counter int64 = 0
|
|
||||||
processMap ProcessMap
|
|
||||||
tunLock sync.Mutex
|
|
||||||
runTime *time.Time
|
|
||||||
errBlocked = errors.New("blocked")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cm *ProcessMap) Store(key int64, value string) {
|
|
||||||
cm.m.Store(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *ProcessMap) Load(key int64) (string, bool) {
|
|
||||||
value, ok := cm.m.Load(key)
|
|
||||||
if !ok || value == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return value.(string), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *FdMap) Store(key int64) {
|
|
||||||
cm.m.Store(key, struct{}{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *FdMap) Load(key int64) bool {
|
|
||||||
_, ok := cm.m.Load(key)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
//export startTUN
|
|
||||||
func startTUN(fd C.int, port C.longlong) {
|
|
||||||
i := int64(port)
|
|
||||||
ServicePort = i
|
|
||||||
if fd == 0 {
|
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
now := time.Now()
|
|
||||||
runTime = &now
|
|
||||||
SendMessage(Message{
|
|
||||||
Type: StartedMessage,
|
|
||||||
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initSocketHook()
|
|
||||||
go func() {
|
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
f := int(fd)
|
|
||||||
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
|
||||||
if tunListener != nil {
|
|
||||||
log.Infoln("TUN address: %v", tunListener.Address())
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
runTime = &now
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getRunTime
|
|
||||||
func getRunTime() *C.char {
|
|
||||||
if runTime == nil {
|
|
||||||
return C.CString("")
|
|
||||||
}
|
|
||||||
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export stopTun
|
|
||||||
func stopTun() {
|
|
||||||
removeSocketHook()
|
|
||||||
go func() {
|
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
|
|
||||||
runTime = nil
|
|
||||||
|
|
||||||
if tunListener != nil {
|
|
||||||
_ = tunListener.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export setFdMap
|
|
||||||
func setFdMap(fd C.long) {
|
|
||||||
fdInt := int64(fd)
|
|
||||||
go func() {
|
|
||||||
fdMap.Store(fdInt)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func markSocket(fd Fd) {
|
|
||||||
SendMessage(Message{
|
|
||||||
Type: ProtectMessage,
|
|
||||||
Data: fd,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSocketHook() {
|
|
||||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
|
||||||
if platform.ShouldBlockConnection() {
|
|
||||||
return errBlocked
|
|
||||||
}
|
|
||||||
return conn.Control(func(fd uintptr) {
|
|
||||||
fdInt := int64(fd)
|
|
||||||
timeout := time.After(500 * time.Millisecond)
|
|
||||||
id := atomic.AddInt64(&fdCounter, 1)
|
|
||||||
|
|
||||||
markSocket(Fd{
|
|
||||||
Id: id,
|
|
||||||
Value: fdInt,
|
|
||||||
})
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
exists := fdMap.Load(id)
|
|
||||||
if exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeSocketHook() {
|
|
||||||
dialer.DefaultSocketHook = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
|
|
||||||
if metadata == nil {
|
|
||||||
return "", process.ErrInvalidNetwork
|
|
||||||
}
|
|
||||||
id := atomic.AddInt64(&counter, 1)
|
|
||||||
|
|
||||||
timeout := time.After(200 * time.Millisecond)
|
|
||||||
|
|
||||||
SendMessage(Message{
|
|
||||||
Type: ProcessMessage,
|
|
||||||
Data: Process{
|
|
||||||
Id: id,
|
|
||||||
Metadata: metadata,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
return "", errors.New("package resolver timeout")
|
|
||||||
default:
|
|
||||||
value, exists := processMap.Load(id)
|
|
||||||
if exists {
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export setProcessMap
|
|
||||||
func setProcessMap(s *C.char) {
|
|
||||||
if s == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
go func() {
|
|
||||||
var processMapItem = &ProcessMapItem{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), processMapItem)
|
|
||||||
if err == nil {
|
|
||||||
processMap.Store(processMapItem.Id, processMapItem.Value)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getCurrentProfileName
|
|
||||||
func getCurrentProfileName() *C.char {
|
|
||||||
if state.CurrentState == nil {
|
|
||||||
return C.CString("")
|
|
||||||
}
|
|
||||||
return C.CString(state.CurrentState.CurrentProfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export getAndroidVpnOptions
|
|
||||||
func getAndroidVpnOptions() *C.char {
|
|
||||||
tunLock.Lock()
|
|
||||||
defer tunLock.Unlock()
|
|
||||||
options := state.AndroidVpnOptions{
|
|
||||||
Enable: state.CurrentState.Enable,
|
|
||||||
Port: currentConfig.General.MixedPort,
|
|
||||||
Ipv4Address: state.DefaultIpv4Address,
|
|
||||||
Ipv6Address: state.GetIpv6Address(),
|
|
||||||
AccessControl: state.CurrentState.AccessControl,
|
|
||||||
SystemProxy: state.CurrentState.SystemProxy,
|
|
||||||
AllowBypass: state.CurrentState.AllowBypass,
|
|
||||||
RouteAddress: state.CurrentState.RouteAddress,
|
|
||||||
BypassDomain: state.CurrentState.BypassDomain,
|
|
||||||
DnsServerAddress: state.GetDnsServerAddress(),
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(options)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error:", err)
|
|
||||||
return C.CString("")
|
|
||||||
}
|
|
||||||
return C.CString(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export setState
|
|
||||||
func setState(s *C.char) {
|
|
||||||
paramsString := C.GoString(s)
|
|
||||||
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
38
core/log.go
Normal file
38
core/log.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"github.com/metacubex/mihomo/common/observable"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logSubscriber observable.Subscription[log.Event]
|
||||||
|
|
||||||
|
//export startLog
|
||||||
|
func startLog() {
|
||||||
|
if logSubscriber != nil {
|
||||||
|
log.UnSubscribe(logSubscriber)
|
||||||
|
logSubscriber = nil
|
||||||
|
}
|
||||||
|
logSubscriber = log.Subscribe()
|
||||||
|
go func() {
|
||||||
|
for logData := range logSubscriber {
|
||||||
|
if logData.LogLevel < log.Level() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
message := &Message{
|
||||||
|
Type: LogMessage,
|
||||||
|
Data: logData,
|
||||||
|
}
|
||||||
|
SendMessage(*message)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export stopLog
|
||||||
|
func stopLog() {
|
||||||
|
if logSubscriber != nil {
|
||||||
|
log.UnSubscribe(logSubscriber)
|
||||||
|
logSubscriber = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
11
core/main.go
11
core/main.go
@@ -1,17 +1,10 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := os.Args
|
fmt.Println("init clash")
|
||||||
if len(args) <= 1 {
|
|
||||||
fmt.Println("Arguments error")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
startServer(args[1])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
//go:build cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,77 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
bridge "core/dart-bridge"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/metacubex/mihomo/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Port int64
|
||||||
|
var ServicePort int64
|
||||||
|
|
||||||
|
type MessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogMessage MessageType = "log"
|
||||||
|
ProtectMessage MessageType = "protect"
|
||||||
|
DelayMessage MessageType = "delay"
|
||||||
|
ProcessMessage MessageType = "process"
|
||||||
|
RequestMessage MessageType = "request"
|
||||||
|
StartedMessage MessageType = "started"
|
||||||
|
LoadedMessage MessageType = "loaded"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Delay struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value int32 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Process struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Metadata *constant.Metadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type MessageType `json:"type"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (message *Message) Json() (string, error) {
|
||||||
|
data, err := json.Marshal(message)
|
||||||
|
return string(data), err
|
||||||
|
}
|
||||||
|
|
||||||
func SendMessage(message Message) {
|
func SendMessage(message Message) {
|
||||||
s, err := message.Json()
|
s, err := message.Json()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Action{
|
if handler, ok := messageHandlers[message.Type]; ok {
|
||||||
Method: messageMethod,
|
handler(s)
|
||||||
}.callback(s)
|
} else {
|
||||||
|
sendToPort(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageHandlers = map[MessageType]func(string) bool{
|
||||||
|
ProtectMessage: sendToServicePort,
|
||||||
|
ProcessMessage: sendToServicePort,
|
||||||
|
StartedMessage: conditionalSend,
|
||||||
|
LoadedMessage: conditionalSend,
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendToPort(s string) bool {
|
||||||
|
return bridge.SendToPort(Port, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendToServicePort(s string) bool {
|
||||||
|
return bridge.SendToPort(ServicePort, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionalSend(s string) bool {
|
||||||
|
isSuccess := sendToPort(s)
|
||||||
|
if !isSuccess {
|
||||||
|
return sendToServicePort(s)
|
||||||
|
}
|
||||||
|
return isSuccess
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
//go:build cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
bridge "core/dart-bridge"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Port int64 = -1
|
|
||||||
ServicePort int64 = -1
|
|
||||||
)
|
|
||||||
|
|
||||||
func SendMessage(message Message) {
|
|
||||||
s, err := message.Json()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if handler, ok := messageHandlers[message.Type]; ok {
|
|
||||||
handler(s)
|
|
||||||
} else {
|
|
||||||
sendToPort(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageHandlers = map[MessageType]func(string) bool{
|
|
||||||
ProtectMessage: sendToServicePort,
|
|
||||||
ProcessMessage: sendToServicePort,
|
|
||||||
StartedMessage: conditionalSend,
|
|
||||||
LoadedMessage: conditionalSend,
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToPort(s string) bool {
|
|
||||||
return bridge.SendToPort(Port, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToServicePort(s string) bool {
|
|
||||||
return bridge.SendToPort(ServicePort, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func conditionalSend(s string) bool {
|
|
||||||
isSuccess := sendToPort(s)
|
|
||||||
if !isSuccess {
|
|
||||||
return sendToServicePort(s)
|
|
||||||
}
|
|
||||||
return isSuccess
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build android && cgo
|
//go:build android
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
81
core/process.go
Normal file
81
core/process.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/metacubex/mihomo/component/process"
|
||||||
|
"github.com/metacubex/mihomo/constant"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessMap struct {
|
||||||
|
m sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ProcessMap) Store(key int64, value string) {
|
||||||
|
cm.m.Store(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ProcessMap) Load(key int64) (string, bool) {
|
||||||
|
value, ok := cm.m.Load(key)
|
||||||
|
if !ok || value == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return value.(string), true
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter int64 = 0
|
||||||
|
|
||||||
|
var processMap ProcessMap
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
|
||||||
|
if metadata == nil {
|
||||||
|
return "", process.ErrInvalidNetwork
|
||||||
|
}
|
||||||
|
id := atomic.AddInt64(&counter, 1)
|
||||||
|
|
||||||
|
timeout := time.After(200 * time.Millisecond)
|
||||||
|
|
||||||
|
SendMessage(Message{
|
||||||
|
Type: ProcessMessage,
|
||||||
|
Data: Process{
|
||||||
|
Id: id,
|
||||||
|
Metadata: metadata,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
return "", errors.New("package resolver timeout")
|
||||||
|
default:
|
||||||
|
value, exists := processMap.Load(id)
|
||||||
|
if exists {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export setProcessMap
|
||||||
|
func setProcessMap(s *C.char) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
paramsString := C.GoString(s)
|
||||||
|
go func() {
|
||||||
|
var processMapItem = &ProcessMapItem{}
|
||||||
|
err := json.Unmarshal([]byte(paramsString), processMapItem)
|
||||||
|
if err == nil {
|
||||||
|
processMap.Store(processMapItem.Id, processMapItem.Value)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
173
core/server.go
173
core/server.go
@@ -1,173 +0,0 @@
|
|||||||
//go:build !cgo
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var conn net.Conn = nil
|
|
||||||
|
|
||||||
func startServer(arg string) {
|
|
||||||
_, err := strconv.Atoi(arg)
|
|
||||||
if err != nil {
|
|
||||||
conn, err = net.Dial("unix", arg)
|
|
||||||
} else {
|
|
||||||
conn, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", arg))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func(conn net.Conn) {
|
|
||||||
_ = conn.Close()
|
|
||||||
}(conn)
|
|
||||||
|
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
|
|
||||||
for {
|
|
||||||
data, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var action = &Action{}
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(data), action)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go handleAction(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAction(action *Action) {
|
|
||||||
switch action.Method {
|
|
||||||
case initClashMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
action.callback(handleInitClash(data))
|
|
||||||
return
|
|
||||||
case getIsInitMethod:
|
|
||||||
action.callback(handleGetIsInit())
|
|
||||||
return
|
|
||||||
case forceGcMethod:
|
|
||||||
handleForceGc()
|
|
||||||
return
|
|
||||||
case shutdownMethod:
|
|
||||||
action.callback(handleShutdown())
|
|
||||||
return
|
|
||||||
case validateConfigMethod:
|
|
||||||
data := []byte(action.Data.(string))
|
|
||||||
action.callback(handleValidateConfig(data))
|
|
||||||
return
|
|
||||||
case updateConfigMethod:
|
|
||||||
data := []byte(action.Data.(string))
|
|
||||||
action.callback(handleUpdateConfig(data))
|
|
||||||
return
|
|
||||||
case getProxiesMethod:
|
|
||||||
action.callback(handleGetProxies())
|
|
||||||
return
|
|
||||||
case changeProxyMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
handleChangeProxy(data, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case getTrafficMethod:
|
|
||||||
data := action.Data.(bool)
|
|
||||||
action.callback(handleGetTraffic(data))
|
|
||||||
return
|
|
||||||
case getTotalTrafficMethod:
|
|
||||||
data := action.Data.(bool)
|
|
||||||
action.callback(handleGetTotalTraffic(data))
|
|
||||||
return
|
|
||||||
case resetTrafficMethod:
|
|
||||||
handleResetTraffic()
|
|
||||||
return
|
|
||||||
case asyncTestDelayMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
handleAsyncTestDelay(data, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case getConnectionsMethod:
|
|
||||||
action.callback(handleGetConnections())
|
|
||||||
return
|
|
||||||
case closeConnectionsMethod:
|
|
||||||
action.callback(handleCloseConnections())
|
|
||||||
return
|
|
||||||
case closeConnectionMethod:
|
|
||||||
id := action.Data.(string)
|
|
||||||
action.callback(handleCloseConnection(id))
|
|
||||||
return
|
|
||||||
case getExternalProvidersMethod:
|
|
||||||
action.callback(handleGetExternalProviders())
|
|
||||||
return
|
|
||||||
case getExternalProviderMethod:
|
|
||||||
externalProviderName := action.Data.(string)
|
|
||||||
action.callback(handleGetExternalProvider(externalProviderName))
|
|
||||||
case updateGeoDataMethod:
|
|
||||||
paramsString := action.Data.(string)
|
|
||||||
var params = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
action.callback(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
geoType := params["geoType"]
|
|
||||||
geoName := params["geoName"]
|
|
||||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case updateExternalProviderMethod:
|
|
||||||
providerName := action.Data.(string)
|
|
||||||
handleUpdateExternalProvider(providerName, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case sideLoadExternalProviderMethod:
|
|
||||||
paramsString := action.Data.(string)
|
|
||||||
var params = map[string]string{}
|
|
||||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
action.callback(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
providerName := params["providerName"]
|
|
||||||
data := params["data"]
|
|
||||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case startLogMethod:
|
|
||||||
handleStartLog()
|
|
||||||
return
|
|
||||||
case stopLogMethod:
|
|
||||||
handleStopLog()
|
|
||||||
return
|
|
||||||
case startListenerMethod:
|
|
||||||
action.callback(handleStartListener())
|
|
||||||
return
|
|
||||||
case stopListenerMethod:
|
|
||||||
action.callback(handleStopListener())
|
|
||||||
return
|
|
||||||
case getCountryCodeMethod:
|
|
||||||
ip := action.Data.(string)
|
|
||||||
handleGetCountryCode(ip, func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case getMemoryMethod:
|
|
||||||
handleGetMemory(func(value string) {
|
|
||||||
action.callback(value)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
46
core/state.go
Normal file
46
core/state.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"core/state"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
//export getCurrentProfileName
|
||||||
|
func getCurrentProfileName() *C.char {
|
||||||
|
if state.CurrentState == nil {
|
||||||
|
return C.CString("")
|
||||||
|
}
|
||||||
|
return C.CString(state.CurrentState.CurrentProfileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getAndroidVpnOptions
|
||||||
|
func getAndroidVpnOptions() *C.char {
|
||||||
|
options := state.AndroidVpnOptions{
|
||||||
|
Enable: state.CurrentState.Enable,
|
||||||
|
Port: state.CurrentRawConfig.MixedPort,
|
||||||
|
Ipv4Address: state.DefaultIpv4Address,
|
||||||
|
Ipv6Address: state.GetIpv6Address(),
|
||||||
|
AccessControl: state.CurrentState.AccessControl,
|
||||||
|
SystemProxy: state.CurrentState.SystemProxy,
|
||||||
|
AllowBypass: state.CurrentState.AllowBypass,
|
||||||
|
BypassDomain: state.CurrentState.BypassDomain,
|
||||||
|
DnsServerAddress: state.GetDnsServerAddress(),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(options)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return C.CString("")
|
||||||
|
}
|
||||||
|
return C.CString(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export setState
|
||||||
|
func setState(s *C.char) {
|
||||||
|
paramsString := C.GoString(s)
|
||||||
|
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
//go:build android && cgo
|
|
||||||
|
|
||||||
package state
|
package state
|
||||||
|
|
||||||
|
import "github.com/metacubex/mihomo/config"
|
||||||
|
|
||||||
var DefaultIpv4Address = "172.19.0.1/30"
|
var DefaultIpv4Address = "172.19.0.1/30"
|
||||||
var DefaultDnsAddress = "172.19.0.2"
|
var DefaultDnsAddress = "172.19.0.2"
|
||||||
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
||||||
|
|
||||||
|
var CurrentRawConfig = config.DefaultRawConfig()
|
||||||
|
|
||||||
type AndroidVpnOptions struct {
|
type AndroidVpnOptions struct {
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
@@ -13,7 +15,6 @@ type AndroidVpnOptions struct {
|
|||||||
AllowBypass bool `json:"allowBypass"`
|
AllowBypass bool `json:"allowBypass"`
|
||||||
SystemProxy bool `json:"systemProxy"`
|
SystemProxy bool `json:"systemProxy"`
|
||||||
BypassDomain []string `json:"bypassDomain"`
|
BypassDomain []string `json:"bypassDomain"`
|
||||||
RouteAddress []string `json:"routeAddress"`
|
|
||||||
Ipv4Address string `json:"ipv4Address"`
|
Ipv4Address string `json:"ipv4Address"`
|
||||||
Ipv6Address string `json:"ipv6Address"`
|
Ipv6Address string `json:"ipv6Address"`
|
||||||
DnsServerAddress string `json:"dnsServerAddress"`
|
DnsServerAddress string `json:"dnsServerAddress"`
|
||||||
@@ -31,7 +32,6 @@ type AndroidVpnRawOptions struct {
|
|||||||
AccessControl *AccessControl `json:"accessControl"`
|
AccessControl *AccessControl `json:"accessControl"`
|
||||||
AllowBypass bool `json:"allowBypass"`
|
AllowBypass bool `json:"allowBypass"`
|
||||||
SystemProxy bool `json:"systemProxy"`
|
SystemProxy bool `json:"systemProxy"`
|
||||||
RouteAddress []string `json:"routeAddress"`
|
|
||||||
Ipv6 bool `json:"ipv6"`
|
Ipv6 bool `json:"ipv6"`
|
||||||
BypassDomain []string `json:"bypassDomain"`
|
BypassDomain []string `json:"bypassDomain"`
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,7 @@ type AndroidVpnRawOptions struct {
|
|||||||
type State struct {
|
type State struct {
|
||||||
AndroidVpnRawOptions
|
AndroidVpnRawOptions
|
||||||
CurrentProfileName string `json:"currentProfileName"`
|
CurrentProfileName string `json:"currentProfileName"`
|
||||||
|
OnlyProxy bool `json:"onlyProxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var CurrentState = &State{}
|
var CurrentState = &State{}
|
||||||
@@ -52,5 +53,7 @@ func GetIpv6Address() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDnsServerAddress() string {
|
func GetDnsServerAddress() string {
|
||||||
|
//prefix, _ := netip.ParsePrefix(DefaultIpv4Address)
|
||||||
|
//return prefix.Addr().String()
|
||||||
return DefaultDnsAddress
|
return DefaultDnsAddress
|
||||||
}
|
}
|
||||||
|
|||||||
157
core/tun.go
Normal file
157
core/tun.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"core/platform"
|
||||||
|
t "core/tun"
|
||||||
|
"errors"
|
||||||
|
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/component/dialer"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tunLock sync.Mutex
|
||||||
|
var runTime *time.Time
|
||||||
|
|
||||||
|
type FdMap struct {
|
||||||
|
m sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *FdMap) Store(key int64) {
|
||||||
|
cm.m.Store(key, struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *FdMap) Load(key int64) bool {
|
||||||
|
_, ok := cm.m.Load(key)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tunListener *sing_tun.Listener
|
||||||
|
fdMap FdMap
|
||||||
|
fdCounter int64 = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
//export startTUN
|
||||||
|
func startTUN(fd C.int, port C.longlong) {
|
||||||
|
i := int64(port)
|
||||||
|
ServicePort = i
|
||||||
|
if fd == 0 {
|
||||||
|
tunLock.Lock()
|
||||||
|
defer tunLock.Unlock()
|
||||||
|
now := time.Now()
|
||||||
|
runTime = &now
|
||||||
|
SendMessage(Message{
|
||||||
|
Type: StartedMessage,
|
||||||
|
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initSocketHook()
|
||||||
|
go func() {
|
||||||
|
tunLock.Lock()
|
||||||
|
defer tunLock.Unlock()
|
||||||
|
f := int(fd)
|
||||||
|
tunListener, _ = t.Start(f)
|
||||||
|
if tunListener != nil {
|
||||||
|
log.Infoln("TUN address: %v", tunListener.Address())
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
runTime = &now
|
||||||
|
|
||||||
|
SendMessage(Message{
|
||||||
|
Type: StartedMessage,
|
||||||
|
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export getRunTime
|
||||||
|
func getRunTime() *C.char {
|
||||||
|
if runTime == nil {
|
||||||
|
return C.CString("")
|
||||||
|
}
|
||||||
|
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export stopTun
|
||||||
|
func stopTun() {
|
||||||
|
removeSocketHook()
|
||||||
|
go func() {
|
||||||
|
tunLock.Lock()
|
||||||
|
defer tunLock.Unlock()
|
||||||
|
|
||||||
|
runTime = nil
|
||||||
|
|
||||||
|
if tunListener != nil {
|
||||||
|
_ = tunListener.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBlocked = errors.New("blocked")
|
||||||
|
|
||||||
|
//export setFdMap
|
||||||
|
func setFdMap(fd C.long) {
|
||||||
|
fdInt := int64(fd)
|
||||||
|
go func() {
|
||||||
|
fdMap.Store(fdInt)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fd struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Value int64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func markSocket(fd Fd) {
|
||||||
|
SendMessage(Message{
|
||||||
|
Type: ProtectMessage,
|
||||||
|
Data: fd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSocketHook() {
|
||||||
|
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||||
|
if platform.ShouldBlockConnection() {
|
||||||
|
return errBlocked
|
||||||
|
}
|
||||||
|
return conn.Control(func(fd uintptr) {
|
||||||
|
fdInt := int64(fd)
|
||||||
|
timeout := time.After(100 * time.Millisecond)
|
||||||
|
id := atomic.AddInt64(&fdCounter, 1)
|
||||||
|
|
||||||
|
markSocket(Fd{
|
||||||
|
Id: id,
|
||||||
|
Value: fdInt,
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
exists := fdMap.Load(id)
|
||||||
|
if exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSocketHook() {
|
||||||
|
dialer.DefaultSocketHook = nil
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
//go:build android && cgo
|
//go:build android
|
||||||
|
|
||||||
package tun
|
package tun
|
||||||
|
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"core/state"
|
"core/state"
|
||||||
"github.com/metacubex/mihomo/constant"
|
|
||||||
LC "github.com/metacubex/mihomo/listener/config"
|
LC "github.com/metacubex/mihomo/listener/config"
|
||||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
@@ -24,7 +23,7 @@ type Props struct {
|
|||||||
Dns6 string `json:"dns6"`
|
Dns6 string `json:"dns6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener, error) {
|
func Start(fd int) (*sing_tun.Listener, error) {
|
||||||
var prefix4 []netip.Prefix
|
var prefix4 []netip.Prefix
|
||||||
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
|
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,8 +46,8 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
|
|||||||
|
|
||||||
options := LC.Tun{
|
options := LC.Tun{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
Device: device,
|
Device: state.CurrentRawConfig.Tun.Device,
|
||||||
Stack: stack,
|
Stack: state.CurrentRawConfig.Tun.Stack,
|
||||||
DNSHijack: dnsHijack,
|
DNSHijack: dnsHijack,
|
||||||
AutoRoute: false,
|
AutoRoute: false,
|
||||||
AutoDetectInterface: false,
|
AutoDetectInterface: false,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/common/common.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/manager/hotkey_manager.dart';
|
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||||
import 'package:fl_clash/manager/manager.dart';
|
import 'package:fl_clash/manager/manager.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.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';
|
||||||
@@ -20,7 +17,6 @@ runAppWithPreferences(
|
|||||||
Widget child, {
|
Widget child, {
|
||||||
required AppState appState,
|
required AppState appState,
|
||||||
required Config config,
|
required Config config,
|
||||||
required AppFlowingState appFlowingState,
|
|
||||||
required ClashConfig clashConfig,
|
required ClashConfig clashConfig,
|
||||||
}) {
|
}) {
|
||||||
runApp(MultiProvider(
|
runApp(MultiProvider(
|
||||||
@@ -32,7 +28,7 @@ runAppWithPreferences(
|
|||||||
create: (_) => config,
|
create: (_) => config,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider<AppFlowingState>(
|
ChangeNotifierProvider<AppFlowingState>(
|
||||||
create: (_) => appFlowingState,
|
create: (_) => AppFlowingState(),
|
||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
||||||
create: (_) => appState,
|
create: (_) => appState,
|
||||||
@@ -58,15 +54,14 @@ class Application extends StatefulWidget {
|
|||||||
|
|
||||||
class ApplicationState extends State<Application> {
|
class ApplicationState extends State<Application> {
|
||||||
late SystemColorSchemes systemColorSchemes;
|
late SystemColorSchemes systemColorSchemes;
|
||||||
Timer? _autoUpdateGroupTaskTimer;
|
Timer? timer;
|
||||||
Timer? _autoUpdateProfilesTaskTimer;
|
|
||||||
|
|
||||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||||
TargetPlatform.android: CommonPageTransitionsBuilder(),
|
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
|
||||||
TargetPlatform.windows: CommonPageTransitionsBuilder(),
|
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
|
||||||
TargetPlatform.linux: CommonPageTransitionsBuilder(),
|
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
|
||||||
TargetPlatform.macOS: CommonPageTransitionsBuilder(),
|
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,8 +83,7 @@ class ApplicationState extends State<Application> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_autoUpdateGroupTask();
|
_initTimer();
|
||||||
_autoUpdateProfilesTask();
|
|
||||||
globalState.appController = AppController(context);
|
globalState.appController = AppController(context);
|
||||||
globalState.measure = Measure.of(context);
|
globalState.measure = Measure.of(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
@@ -99,33 +93,32 @@ class ApplicationState extends State<Application> {
|
|||||||
}
|
}
|
||||||
await globalState.appController.init();
|
await globalState.appController.init();
|
||||||
globalState.appController.initLink();
|
globalState.appController.initLink();
|
||||||
app?.initShortcuts();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_autoUpdateGroupTask() {
|
_initTimer() {
|
||||||
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
|
_cancelTimer();
|
||||||
|
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
globalState.appController.updateGroupsDebounce();
|
globalState.appController.updateGroupDebounce();
|
||||||
_autoUpdateGroupTask();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_autoUpdateProfilesTask() {
|
_cancelTimer() {
|
||||||
_autoUpdateProfilesTaskTimer = Timer(const Duration(seconds: 5), () async {
|
if (timer != null) {
|
||||||
await globalState.appController.autoUpdateProfiles();
|
timer?.cancel();
|
||||||
_autoUpdateProfilesTask();
|
timer = null;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildPlatformWrap(Widget child) {
|
_buildApp(Widget app) {
|
||||||
if (system.isDesktop) {
|
if (system.isDesktop) {
|
||||||
return WindowManager(
|
return WindowManager(
|
||||||
child: TrayManager(
|
child: TrayManager(
|
||||||
child: HotKeyManager(
|
child: HotKeyManager(
|
||||||
child: ProxyManager(
|
child: ProxyManager(
|
||||||
child: child,
|
child: app,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -133,7 +126,7 @@ class ApplicationState extends State<Application> {
|
|||||||
}
|
}
|
||||||
return AndroidManager(
|
return AndroidManager(
|
||||||
child: TileManager(
|
child: TileManager(
|
||||||
child: child,
|
child: app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,17 +142,6 @@ class ApplicationState extends State<Application> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildWrap(Widget child) {
|
|
||||||
return AppStateManager(
|
|
||||||
child: ClashManager(
|
|
||||||
child: ConnectivityManager(
|
|
||||||
onConnectivityChanged: globalState.appController.updateLocalIp,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateSystemColorSchemes(
|
_updateSystemColorSchemes(
|
||||||
ColorScheme? lightDynamic,
|
ColorScheme? lightDynamic,
|
||||||
ColorScheme? darkDynamic,
|
ColorScheme? darkDynamic,
|
||||||
@@ -175,31 +157,30 @@ class ApplicationState extends State<Application> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(context) {
|
Widget build(context) {
|
||||||
return _buildWrap(
|
return _buildApp(
|
||||||
_buildPlatformWrap(
|
AppStateManager(
|
||||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
child: ClashManager(
|
||||||
selector: (_, appState, config) => ApplicationSelectorState(
|
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||||
locale: config.appSetting.locale,
|
selector: (_, appState, config) => ApplicationSelectorState(
|
||||||
themeMode: config.themeProps.themeMode,
|
locale: config.appSetting.locale,
|
||||||
primaryColor: config.themeProps.primaryColor,
|
themeMode: config.themeMode,
|
||||||
prueBlack: config.themeProps.prueBlack,
|
primaryColor: config.primaryColor,
|
||||||
fontFamily: config.themeProps.fontFamily,
|
prueBlack: config.prueBlack,
|
||||||
),
|
),
|
||||||
builder: (_, state, child) {
|
builder: (_, state, child) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
navigatorKey: globalState.navigatorKey,
|
navigatorKey: globalState.navigatorKey,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
AppLocalizations.delegate,
|
AppLocalizations.delegate,
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate
|
GlobalWidgetsLocalizations.delegate
|
||||||
],
|
],
|
||||||
builder: (_, child) {
|
builder: (_, child) {
|
||||||
return MessageManager(
|
return LayoutBuilder(
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (_, container) {
|
builder: (_, container) {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final maxWidth = container.maxWidth;
|
final maxWidth = container.maxWidth;
|
||||||
@@ -208,40 +189,39 @@ class ApplicationState extends State<Application> {
|
|||||||
}
|
}
|
||||||
return _buildPage(child!);
|
return _buildPage(child!);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
scrollBehavior: BaseScrollBehavior(),
|
||||||
|
title: appName,
|
||||||
|
locale: other.getLocaleForString(state.locale),
|
||||||
|
supportedLocales:
|
||||||
|
AppLocalizations.delegate.supportedLocales,
|
||||||
|
themeMode: state.themeMode,
|
||||||
|
theme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
pageTransitionsTheme: _pageTransitionsTheme,
|
||||||
|
colorScheme: _getAppColorScheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
systemColorSchemes: systemColorSchemes,
|
||||||
|
primaryColor: state.primaryColor,
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
scrollBehavior: BaseScrollBehavior(),
|
|
||||||
title: appName,
|
|
||||||
locale: other.getLocaleForString(state.locale),
|
|
||||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
|
||||||
themeMode: state.themeMode,
|
|
||||||
theme: ThemeData(
|
|
||||||
useMaterial3: true,
|
|
||||||
fontFamily: state.fontFamily.value,
|
|
||||||
pageTransitionsTheme: _pageTransitionsTheme,
|
|
||||||
colorScheme: _getAppColorScheme(
|
|
||||||
brightness: Brightness.light,
|
|
||||||
systemColorSchemes: systemColorSchemes,
|
|
||||||
primaryColor: state.primaryColor,
|
|
||||||
),
|
),
|
||||||
),
|
darkTheme: ThemeData(
|
||||||
darkTheme: ThemeData(
|
useMaterial3: true,
|
||||||
useMaterial3: true,
|
pageTransitionsTheme: _pageTransitionsTheme,
|
||||||
fontFamily: state.fontFamily.value,
|
colorScheme: _getAppColorScheme(
|
||||||
pageTransitionsTheme: _pageTransitionsTheme,
|
brightness: Brightness.dark,
|
||||||
colorScheme: _getAppColorScheme(
|
systemColorSchemes: systemColorSchemes,
|
||||||
brightness: Brightness.dark,
|
primaryColor: state.primaryColor,
|
||||||
systemColorSchemes: systemColorSchemes,
|
).toPrueBlack(state.prueBlack),
|
||||||
primaryColor: state.primaryColor,
|
),
|
||||||
).toPrueBlack(state.prueBlack),
|
home: child,
|
||||||
),
|
);
|
||||||
home: child,
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
child: const HomePage(),
|
||||||
},
|
),
|
||||||
child: const HomePage(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -250,11 +230,8 @@ class ApplicationState extends State<Application> {
|
|||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
linkManager.destroy();
|
linkManager.destroy();
|
||||||
_autoUpdateGroupTaskTimer?.cancel();
|
|
||||||
_autoUpdateProfilesTaskTimer?.cancel();
|
|
||||||
await clashService?.destroy();
|
|
||||||
await globalState.appController.savePreferences();
|
await globalState.appController.savePreferences();
|
||||||
await globalState.appController.handleExit();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
_cancelTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export 'core.dart';
|
export 'core.dart';
|
||||||
export 'lib.dart';
|
|
||||||
export 'message.dart';
|
|
||||||
export 'service.dart';
|
export 'service.dart';
|
||||||
|
export 'message.dart';
|
||||||
@@ -1,26 +1,41 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:fl_clash/clash/interface.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:flutter/services.dart';
|
import 'generated/clash_ffi.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
class ClashCore {
|
class ClashCore {
|
||||||
static ClashCore? _instance;
|
static ClashCore? _instance;
|
||||||
late ClashInterface clashInterface;
|
static final receiver = ReceivePort();
|
||||||
|
|
||||||
|
late final ClashFFI clashFFI;
|
||||||
|
late final DynamicLibrary lib;
|
||||||
|
|
||||||
|
DynamicLibrary _getClashLib() {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return DynamicLibrary.open("libclash.dll");
|
||||||
|
}
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
return DynamicLibrary.open("libclash.dylib");
|
||||||
|
}
|
||||||
|
if (Platform.isAndroid || Platform.isLinux) {
|
||||||
|
return DynamicLibrary.open("libclash.so");
|
||||||
|
}
|
||||||
|
throw "Platform is not supported";
|
||||||
|
}
|
||||||
|
|
||||||
ClashCore._internal() {
|
ClashCore._internal() {
|
||||||
if (Platform.isAndroid) {
|
lib = _getClashLib();
|
||||||
clashInterface = clashLib!;
|
clashFFI = ClashFFI(lib);
|
||||||
} else {
|
clashFFI.initNativeApiBridge(
|
||||||
clashInterface = clashService!;
|
NativeApi.initializeApiDLData,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ClashCore() {
|
factory ClashCore() {
|
||||||
@@ -28,62 +43,67 @@ class ClashCore {
|
|||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initGeo() async {
|
bool init(String homeDir) {
|
||||||
final homePath = await appPath.getHomeDirPath();
|
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
|
||||||
final homeDir = Directory(homePath);
|
final isInit = clashFFI.initClash(homeDirChar) == 1;
|
||||||
final isExists = await homeDir.exists();
|
malloc.free(homeDirChar);
|
||||||
if (!isExists) {
|
return isInit;
|
||||||
await homeDir.create(recursive: true);
|
}
|
||||||
}
|
|
||||||
const geoFileNameList = [
|
shutdown() {
|
||||||
mmdbFileName,
|
clashFFI.shutdownClash();
|
||||||
geoIpFileName,
|
lib.close();
|
||||||
geoSiteFileName,
|
}
|
||||||
asnFileName,
|
|
||||||
];
|
bool get isInit => clashFFI.getIsInit() == 1;
|
||||||
try {
|
|
||||||
for (final geoFileName in geoFileNameList) {
|
Future<String> validateConfig(String data) {
|
||||||
final geoFile = File(
|
final completer = Completer<String>();
|
||||||
join(homePath, geoFileName),
|
final receiver = ReceivePort();
|
||||||
);
|
receiver.listen((message) {
|
||||||
final isExists = await geoFile.exists();
|
if (!completer.isCompleted) {
|
||||||
if (isExists) {
|
completer.complete(message);
|
||||||
continue;
|
receiver.close();
|
||||||
}
|
|
||||||
final data = await rootBundle.load('assets/data/$geoFileName');
|
|
||||||
List<int> bytes = data.buffer.asUint8List();
|
|
||||||
await geoFile.writeAsBytes(bytes, flush: true);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
});
|
||||||
exit(0);
|
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||||
}
|
clashFFI.validateConfig(
|
||||||
|
dataChar,
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
|
malloc.free(dataChar);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> init({
|
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
|
||||||
required ClashConfig clashConfig,
|
final completer = Completer<String>();
|
||||||
required Config config,
|
final receiver = ReceivePort();
|
||||||
}) async {
|
receiver.listen((message) {
|
||||||
await _initGeo();
|
if (!completer.isCompleted) {
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
completer.complete(message);
|
||||||
return await clashInterface.init(homeDirPath);
|
receiver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final params = json.encode(updateConfigParams);
|
||||||
|
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.updateConfig(
|
||||||
|
paramsChar,
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
|
malloc.free(paramsChar);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() async {
|
initMessage() {
|
||||||
await clashInterface.shutdown();
|
clashFFI.initMessage(
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<bool> get isInit => clashInterface.isInit;
|
Future<List<Group>> getProxiesGroups() {
|
||||||
|
final proxiesRaw = clashFFI.getProxies();
|
||||||
FutureOr<String> validateConfig(String data) {
|
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
||||||
return clashInterface.validateConfig(data);
|
clashFFI.freeCString(proxiesRaw);
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
|
||||||
return await clashInterface.updateConfig(updateConfigParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Group>> getProxiesGroups() async {
|
|
||||||
final proxiesRawString = await clashInterface.getProxies();
|
|
||||||
return Isolate.run<List<Group>>(() {
|
return Isolate.run<List<Group>>(() {
|
||||||
if (proxiesRawString.isEmpty) return [];
|
if (proxiesRawString.isEmpty) return [];
|
||||||
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
|
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
|
||||||
@@ -113,128 +133,256 @@ class ClashCore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
|
Future<List<ExternalProvider>> getExternalProviders() {
|
||||||
return await clashInterface.changeProxy(changeProxyParams);
|
final externalProvidersRaw = clashFFI.getExternalProviders();
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Connection>> getConnections() async {
|
|
||||||
final res = await clashInterface.getConnections();
|
|
||||||
final connectionsData = json.decode(res) as Map;
|
|
||||||
final connectionsRaw = connectionsData['connections'] as List? ?? [];
|
|
||||||
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeConnection(String id) {
|
|
||||||
clashInterface.closeConnection(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeConnections() {
|
|
||||||
clashInterface.closeConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
|
||||||
final externalProvidersRawString =
|
final externalProvidersRawString =
|
||||||
await clashInterface.getExternalProviders();
|
externalProvidersRaw.cast<Utf8>().toDartString();
|
||||||
return Isolate.run<List<ExternalProvider>>(
|
clashFFI.freeCString(externalProvidersRaw);
|
||||||
() {
|
return Isolate.run<List<ExternalProvider>>(() {
|
||||||
final externalProviders =
|
final externalProviders =
|
||||||
(json.decode(externalProvidersRawString) as List<dynamic>)
|
(json.decode(externalProvidersRawString) as List<dynamic>)
|
||||||
.map(
|
.map(
|
||||||
(item) => ExternalProvider.fromJson(item),
|
(item) => ExternalProvider.fromJson(item),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return externalProviders;
|
return externalProviders;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ExternalProvider?> getExternalProvider(
|
ExternalProvider? getExternalProvider(String externalProviderName) {
|
||||||
String externalProviderName) async {
|
final externalProviderNameChar =
|
||||||
final externalProvidersRawString =
|
externalProviderName.toNativeUtf8().cast<Char>();
|
||||||
await clashInterface.getExternalProvider(externalProviderName);
|
final externalProviderRaw =
|
||||||
if (externalProvidersRawString == null) {
|
clashFFI.getExternalProvider(externalProviderNameChar);
|
||||||
return null;
|
malloc.free(externalProviderNameChar);
|
||||||
}
|
final externalProviderRawString =
|
||||||
if (externalProvidersRawString.isEmpty) {
|
externalProviderRaw.cast<Utf8>().toDartString();
|
||||||
return null;
|
clashFFI.freeCString(externalProviderRaw);
|
||||||
}
|
if (externalProviderRawString.isEmpty) return null;
|
||||||
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> updateGeoData({
|
Future<String> updateGeoData({
|
||||||
required String geoType,
|
required String geoType,
|
||||||
required String geoName,
|
required String geoName,
|
||||||
}) {
|
}) {
|
||||||
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
|
final completer = Completer<String>();
|
||||||
|
final receiver = ReceivePort();
|
||||||
|
receiver.listen((message) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(message);
|
||||||
|
receiver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
|
||||||
|
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.updateGeoData(
|
||||||
|
geoTypeChar,
|
||||||
|
geoNameChar,
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
|
malloc.free(geoTypeChar);
|
||||||
|
malloc.free(geoNameChar);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> sideLoadExternalProvider({
|
Future<String> sideLoadExternalProvider({
|
||||||
required String providerName,
|
required String providerName,
|
||||||
required String data,
|
required String data,
|
||||||
}) {
|
}) {
|
||||||
return clashInterface.sideLoadExternalProvider(
|
final completer = Completer<String>();
|
||||||
providerName: providerName, data: data);
|
final receiver = ReceivePort();
|
||||||
|
receiver.listen((message) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(message);
|
||||||
|
receiver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||||
|
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.sideLoadExternalProvider(
|
||||||
|
providerNameChar,
|
||||||
|
dataChar,
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
|
malloc.free(providerNameChar);
|
||||||
|
malloc.free(dataChar);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> updateExternalProvider({
|
Future<String> updateExternalProvider({
|
||||||
required String providerName,
|
required String providerName,
|
||||||
}) async {
|
}) {
|
||||||
return clashInterface.updateExternalProvider(providerName);
|
final completer = Completer<String>();
|
||||||
}
|
final receiver = ReceivePort();
|
||||||
|
receiver.listen((message) {
|
||||||
startListener() async {
|
if (!completer.isCompleted) {
|
||||||
await clashInterface.startListener();
|
completer.complete(message);
|
||||||
}
|
receiver.close();
|
||||||
|
}
|
||||||
stopListener() async {
|
});
|
||||||
await clashInterface.stopListener();
|
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||||
}
|
clashFFI.updateExternalProvider(
|
||||||
|
providerNameChar,
|
||||||
Future<Delay> getDelay(String proxyName) async {
|
receiver.sendPort.nativePort,
|
||||||
final data = await clashInterface.asyncTestDelay(proxyName);
|
|
||||||
return Delay.fromJson(json.decode(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Traffic> getTraffic(bool value) async {
|
|
||||||
final trafficString = await clashInterface.getTraffic(value);
|
|
||||||
return Traffic.fromMap(json.decode(trafficString));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<IpInfo?> getCountryCode(String ip) async {
|
|
||||||
final countryCode = await clashInterface.getCountryCode(ip);
|
|
||||||
if (countryCode.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return IpInfo(
|
|
||||||
ip: ip,
|
|
||||||
countryCode: countryCode,
|
|
||||||
);
|
);
|
||||||
|
malloc.free(providerNameChar);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Traffic> getTotalTraffic(bool value) async {
|
changeProxy(ChangeProxyParams changeProxyParams) {
|
||||||
final totalTrafficString = await clashInterface.getTotalTraffic(value);
|
final params = json.encode(changeProxyParams);
|
||||||
return Traffic.fromMap(json.decode(totalTrafficString));
|
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.changeProxy(paramsChar);
|
||||||
|
malloc.free(paramsChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getMemory() async {
|
start() {
|
||||||
final value = await clashInterface.getMemory();
|
clashFFI.start();
|
||||||
return int.parse(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetTraffic() {
|
stop() {
|
||||||
clashInterface.resetTraffic();
|
clashFFI.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
startLog() {
|
Future<Delay> getDelay(String proxyName) {
|
||||||
clashInterface.startLog();
|
final delayParams = {
|
||||||
|
"proxy-name": proxyName,
|
||||||
|
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||||
|
};
|
||||||
|
final completer = Completer<Delay>();
|
||||||
|
final receiver = ReceivePort();
|
||||||
|
receiver.listen((message) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(Delay.fromJson(json.decode(message)));
|
||||||
|
receiver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final delayParamsChar =
|
||||||
|
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.asyncTestDelay(
|
||||||
|
delayParamsChar,
|
||||||
|
receiver.sendPort.nativePort,
|
||||||
|
);
|
||||||
|
malloc.free(delayParamsChar);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEffect(String profileId) {
|
||||||
|
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.clearEffect(profileIdChar);
|
||||||
|
malloc.free(profileIdChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
VersionInfo getVersionInfo() {
|
||||||
|
final versionInfoRaw = clashFFI.getVersionInfo();
|
||||||
|
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
|
||||||
|
clashFFI.freeCString(versionInfoRaw);
|
||||||
|
return VersionInfo.fromJson(versionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(CoreState state) {
|
||||||
|
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.setState(stateChar);
|
||||||
|
malloc.free(stateChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCurrentProfileName() {
|
||||||
|
final currentProfileRaw = clashFFI.getCurrentProfileName();
|
||||||
|
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
|
||||||
|
clashFFI.freeCString(currentProfileRaw);
|
||||||
|
return currentProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidVpnOptions getAndroidVpnOptions() {
|
||||||
|
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
|
||||||
|
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
|
||||||
|
clashFFI.freeCString(vpnOptionsRaw);
|
||||||
|
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
Traffic getTraffic() {
|
||||||
|
final trafficRaw = clashFFI.getTraffic();
|
||||||
|
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
|
||||||
|
clashFFI.freeCString(trafficRaw);
|
||||||
|
return Traffic.fromMap(trafficMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
Traffic getTotalTraffic() {
|
||||||
|
final trafficRaw = clashFFI.getTotalTraffic();
|
||||||
|
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
|
||||||
|
clashFFI.freeCString(trafficRaw);
|
||||||
|
return Traffic.fromMap(trafficMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetTraffic() {
|
||||||
|
clashFFI.resetTraffic();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startLog() {
|
||||||
|
clashFFI.startLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
stopLog() {
|
stopLog() {
|
||||||
clashInterface.stopLog();
|
clashFFI.stopLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
startTun(int fd, int port) {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
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() {
|
||||||
clashInterface.forceGc();
|
clashFFI.forceGc();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopTun() {
|
||||||
|
clashFFI.stopTun();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setProcessMap(ProcessMapItem processMapItem) {
|
||||||
|
final processMapItemChar =
|
||||||
|
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.setProcessMap(processMapItemChar);
|
||||||
|
malloc.free(processMapItemChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFdMap(int fd) {
|
||||||
|
clashFFI.setFdMap(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? getRunTime() {
|
||||||
|
final runTimeRaw = clashFFI.getRunTime();
|
||||||
|
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||||
|
clashFFI.freeCString(runTimeRaw);
|
||||||
|
if (runTimeString.isEmpty) return null;
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Connection> getConnections() {
|
||||||
|
final connectionsDataRaw = clashFFI.getConnections();
|
||||||
|
final connectionsData =
|
||||||
|
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
|
||||||
|
clashFFI.freeCString(connectionsDataRaw);
|
||||||
|
final connectionsRaw = connectionsData['connections'] as List? ?? [];
|
||||||
|
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeConnection(String id) {
|
||||||
|
final idChar = id.toNativeUtf8().cast<Char>();
|
||||||
|
clashFFI.closeConnection(idChar);
|
||||||
|
malloc.free(idChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeConnections() {
|
||||||
|
clashFFI.closeConnections();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
|
|
||||||
mixin ClashInterface {
|
|
||||||
FutureOr<bool> init(String homeDir);
|
|
||||||
|
|
||||||
FutureOr<void> shutdown();
|
|
||||||
|
|
||||||
FutureOr<bool> get isInit;
|
|
||||||
|
|
||||||
forceGc();
|
|
||||||
|
|
||||||
FutureOr<String> validateConfig(String data);
|
|
||||||
|
|
||||||
Future<String> asyncTestDelay(String proxyName);
|
|
||||||
|
|
||||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
|
||||||
|
|
||||||
FutureOr<String> getProxies();
|
|
||||||
|
|
||||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
|
|
||||||
|
|
||||||
Future<bool> startListener();
|
|
||||||
|
|
||||||
Future<bool> stopListener();
|
|
||||||
|
|
||||||
FutureOr<String> getExternalProviders();
|
|
||||||
|
|
||||||
FutureOr<String>? getExternalProvider(String externalProviderName);
|
|
||||||
|
|
||||||
Future<String> updateGeoData({
|
|
||||||
required String geoType,
|
|
||||||
required String geoName,
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<String> sideLoadExternalProvider({
|
|
||||||
required String providerName,
|
|
||||||
required String data,
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<String> updateExternalProvider(String providerName);
|
|
||||||
|
|
||||||
FutureOr<String> getTraffic(bool value);
|
|
||||||
|
|
||||||
FutureOr<String> getTotalTraffic(bool value);
|
|
||||||
|
|
||||||
FutureOr<String> getCountryCode(String ip);
|
|
||||||
|
|
||||||
FutureOr<String> getMemory();
|
|
||||||
|
|
||||||
resetTraffic();
|
|
||||||
|
|
||||||
startLog();
|
|
||||||
|
|
||||||
stopLog();
|
|
||||||
|
|
||||||
FutureOr<String> getConnections();
|
|
||||||
|
|
||||||
FutureOr<bool> closeConnection(String id);
|
|
||||||
|
|
||||||
FutureOr<bool> closeConnections();
|
|
||||||
}
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:fl_clash/common/constant.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
|
|
||||||
import 'generated/clash_ffi.dart';
|
|
||||||
import 'interface.dart';
|
|
||||||
|
|
||||||
class ClashLib with ClashInterface {
|
|
||||||
static ClashLib? _instance;
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
|
|
||||||
late final ClashFFI clashFFI;
|
|
||||||
|
|
||||||
late final DynamicLibrary lib;
|
|
||||||
|
|
||||||
ClashLib._internal() {
|
|
||||||
lib = DynamicLibrary.open("libclash.so");
|
|
||||||
clashFFI = ClashFFI(lib);
|
|
||||||
clashFFI.initNativeApiBridge(
|
|
||||||
NativeApi.initializeApiDLData,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ClashLib() {
|
|
||||||
_instance ??= ClashLib._internal();
|
|
||||||
return _instance!;
|
|
||||||
}
|
|
||||||
|
|
||||||
initMessage() {
|
|
||||||
clashFFI.initMessage(
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool init(String homeDir) {
|
|
||||||
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
|
|
||||||
final isInit = clashFFI.initClash(homeDirChar) == 1;
|
|
||||||
malloc.free(homeDirChar);
|
|
||||||
return isInit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
shutdown() async {
|
|
||||||
clashFFI.shutdownClash();
|
|
||||||
lib.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isInit => clashFFI.getIsInit() == 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> validateConfig(String data) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.validateConfig(
|
|
||||||
dataChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(dataChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final params = json.encode(updateConfigParams);
|
|
||||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateConfig(
|
|
||||||
paramsChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(paramsChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getProxies() {
|
|
||||||
final proxiesRaw = clashFFI.getProxies();
|
|
||||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(proxiesRaw);
|
|
||||||
return proxiesRawString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getExternalProviders() {
|
|
||||||
final externalProvidersRaw = clashFFI.getExternalProviders();
|
|
||||||
final externalProvidersRawString =
|
|
||||||
externalProvidersRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(externalProvidersRaw);
|
|
||||||
return externalProvidersRawString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getExternalProvider(String externalProviderName) {
|
|
||||||
final externalProviderNameChar =
|
|
||||||
externalProviderName.toNativeUtf8().cast<Char>();
|
|
||||||
final externalProviderRaw =
|
|
||||||
clashFFI.getExternalProvider(externalProviderNameChar);
|
|
||||||
malloc.free(externalProviderNameChar);
|
|
||||||
final externalProviderRawString =
|
|
||||||
externalProviderRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(externalProviderRaw);
|
|
||||||
return externalProviderRawString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateGeoData({
|
|
||||||
required String geoType,
|
|
||||||
required String geoName,
|
|
||||||
}) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
|
|
||||||
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateGeoData(
|
|
||||||
geoTypeChar,
|
|
||||||
geoNameChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(geoTypeChar);
|
|
||||||
malloc.free(geoNameChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> sideLoadExternalProvider({
|
|
||||||
required String providerName,
|
|
||||||
required String data,
|
|
||||||
}) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
|
||||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.sideLoadExternalProvider(
|
|
||||||
providerNameChar,
|
|
||||||
dataChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(providerNameChar);
|
|
||||||
malloc.free(dataChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateExternalProvider(String providerName) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateExternalProvider(
|
|
||||||
providerNameChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(providerNameChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final params = json.encode(changeProxyParams);
|
|
||||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.changeProxy(
|
|
||||||
paramsChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(paramsChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getConnections() {
|
|
||||||
final connectionsDataRaw = clashFFI.getConnections();
|
|
||||||
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(connectionsDataRaw);
|
|
||||||
return connectionsString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
closeConnection(String id) {
|
|
||||||
final idChar = id.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.closeConnection(idChar);
|
|
||||||
malloc.free(idChar);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
closeConnections() {
|
|
||||||
clashFFI.closeConnections();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
startListener() async {
|
|
||||||
clashFFI.startListener();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopListener() async {
|
|
||||||
clashFFI.stopListener();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> asyncTestDelay(String proxyName) {
|
|
||||||
final delayParams = {
|
|
||||||
"proxy-name": proxyName,
|
|
||||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
|
||||||
};
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final delayParamsChar =
|
|
||||||
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.asyncTestDelay(
|
|
||||||
delayParamsChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(delayParamsChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getTraffic(bool value) {
|
|
||||||
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0);
|
|
||||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(trafficRaw);
|
|
||||||
return trafficString;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getTotalTraffic(bool value) {
|
|
||||||
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
|
|
||||||
clashFFI.freeCString(trafficRaw);
|
|
||||||
return trafficRaw.cast<Utf8>().toDartString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void resetTraffic() {
|
|
||||||
clashFFI.resetTraffic();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void startLog() {
|
|
||||||
clashFFI.startLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopLog() {
|
|
||||||
clashFFI.stopLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
forceGc() {
|
|
||||||
clashFFI.forceGc();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getCountryCode(String ip) {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final ipChar = ip.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.getCountryCode(
|
|
||||||
ipChar,
|
|
||||||
receiver.sendPort.nativePort,
|
|
||||||
);
|
|
||||||
malloc.free(ipChar);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getMemory() {
|
|
||||||
final completer = Completer<String>();
|
|
||||||
final receiver = ReceivePort();
|
|
||||||
receiver.listen((message) {
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.complete(message);
|
|
||||||
receiver.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
clashFFI.getMemory(receiver.sendPort.nativePort);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Android
|
|
||||||
|
|
||||||
startTun(int fd, int port) {
|
|
||||||
if (!Platform.isAndroid) return;
|
|
||||||
clashFFI.startTUN(fd, port);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopTun() {
|
|
||||||
clashFFI.stopTun();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDns(String dns) {
|
|
||||||
if (!Platform.isAndroid) return;
|
|
||||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.updateDns(dnsChar);
|
|
||||||
malloc.free(dnsChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProcessMap(ProcessMapItem processMapItem) {
|
|
||||||
final processMapItemChar =
|
|
||||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.setProcessMap(processMapItemChar);
|
|
||||||
malloc.free(processMapItemChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(CoreState state) {
|
|
||||||
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
|
||||||
clashFFI.setState(stateChar);
|
|
||||||
malloc.free(stateChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getCurrentProfileName() {
|
|
||||||
final currentProfileRaw = clashFFI.getCurrentProfileName();
|
|
||||||
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(currentProfileRaw);
|
|
||||||
return currentProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
AndroidVpnOptions getAndroidVpnOptions() {
|
|
||||||
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
|
|
||||||
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
|
|
||||||
clashFFI.freeCString(vpnOptionsRaw);
|
|
||||||
return AndroidVpnOptions.fromJson(vpnOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFdMap(int fd) {
|
|
||||||
clashFFI.setFdMap(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? getRunTime() {
|
|
||||||
final runTimeRaw = clashFFI.getRunTime();
|
|
||||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
|
||||||
clashFFI.freeCString(runTimeRaw);
|
|
||||||
if (runTimeString.isEmpty) return null;
|
|
||||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final clashLib = Platform.isAndroid ? ClashLib() : null;
|
|
||||||
@@ -1,40 +1,42 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.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:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'core.dart';
|
||||||
|
|
||||||
class ClashMessage {
|
class ClashMessage {
|
||||||
final controller = StreamController();
|
StreamSubscription? subscription;
|
||||||
|
|
||||||
ClashMessage._() {
|
ClashMessage._() {
|
||||||
clashLib?.receiver.listen(controller.add);
|
if (subscription != null) {
|
||||||
controller.stream.listen(
|
subscription!.cancel();
|
||||||
(message) {
|
subscription = null;
|
||||||
final m = AppMessage.fromJson(json.decode(message));
|
}
|
||||||
for (final AppMessageListener listener in _listeners) {
|
subscription = ClashCore.receiver.listen((message) {
|
||||||
switch (m.type) {
|
final m = AppMessage.fromJson(json.decode(message));
|
||||||
case AppMessageType.log:
|
for (final AppMessageListener listener in _listeners) {
|
||||||
listener.onLog(Log.fromJson(m.data));
|
switch (m.type) {
|
||||||
break;
|
case AppMessageType.log:
|
||||||
case AppMessageType.delay:
|
listener.onLog(Log.fromJson(m.data));
|
||||||
listener.onDelay(Delay.fromJson(m.data));
|
break;
|
||||||
break;
|
case AppMessageType.delay:
|
||||||
case AppMessageType.request:
|
listener.onDelay(Delay.fromJson(m.data));
|
||||||
listener.onRequest(Connection.fromJson(m.data));
|
break;
|
||||||
break;
|
case AppMessageType.request:
|
||||||
case AppMessageType.started:
|
listener.onRequest(Connection.fromJson(m.data));
|
||||||
listener.onStarted(m.data);
|
break;
|
||||||
break;
|
case AppMessageType.started:
|
||||||
case AppMessageType.loaded:
|
listener.onStarted(m.data);
|
||||||
listener.onLoaded(m.data);
|
break;
|
||||||
break;
|
case AppMessageType.loaded:
|
||||||
}
|
listener.onLoaded(m.data);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static final ClashMessage instance = ClashMessage._();
|
static final ClashMessage instance = ClashMessage._();
|
||||||
|
|||||||
@@ -1,439 +1,54 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/clash/interface.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/models/models.dart';
|
||||||
import 'package:fl_clash/models/core.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class ClashService with ClashInterface {
|
import 'core.dart';
|
||||||
static ClashService? _instance;
|
|
||||||
|
|
||||||
Completer<ServerSocket> serverCompleter = Completer();
|
class ClashService {
|
||||||
|
Future<void> initGeo() async {
|
||||||
Completer<Socket> socketCompleter = Completer();
|
final homePath = await appPath.getHomeDirPath();
|
||||||
|
final homeDir = Directory(homePath);
|
||||||
Map<String, Completer> callbackCompleterMap = {};
|
final isExists = await homeDir.exists();
|
||||||
|
if (!isExists) {
|
||||||
Process? process;
|
await homeDir.create(recursive: true);
|
||||||
|
|
||||||
factory ClashService() {
|
|
||||||
_instance ??= ClashService._internal();
|
|
||||||
return _instance!;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClashService._internal() {
|
|
||||||
_createServer();
|
|
||||||
startCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
_createServer() async {
|
|
||||||
final address = !Platform.isWindows
|
|
||||||
? InternetAddress(
|
|
||||||
unixSocketPath,
|
|
||||||
type: InternetAddressType.unix,
|
|
||||||
)
|
|
||||||
: InternetAddress(
|
|
||||||
localhost,
|
|
||||||
type: InternetAddressType.IPv4,
|
|
||||||
);
|
|
||||||
await _deleteSocketFile();
|
|
||||||
final server = await ServerSocket.bind(
|
|
||||||
address,
|
|
||||||
0,
|
|
||||||
shared: true,
|
|
||||||
);
|
|
||||||
serverCompleter.complete(server);
|
|
||||||
await for (final socket in server) {
|
|
||||||
await _destroySocket();
|
|
||||||
socketCompleter.complete(socket);
|
|
||||||
socket
|
|
||||||
.transform(
|
|
||||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
|
||||||
handleData: (Uint8List data, EventSink<String> sink) {
|
|
||||||
sink.add(utf8.decode(data, allowMalformed: true));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.transform(LineSplitter())
|
|
||||||
.listen(
|
|
||||||
(data) {
|
|
||||||
_handleAction(
|
|
||||||
Action.fromJson(
|
|
||||||
json.decode(data.trim()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
const geoFileNameList = [
|
||||||
|
mmdbFileName,
|
||||||
startCore() async {
|
geoIpFileName,
|
||||||
if (process != null) {
|
geoSiteFileName,
|
||||||
await shutdown();
|
asnFileName,
|
||||||
}
|
];
|
||||||
final serverSocket = await serverCompleter.future;
|
try {
|
||||||
final arg = Platform.isWindows
|
for (final geoFileName in geoFileNameList) {
|
||||||
? "${serverSocket.port}"
|
final geoFile = File(
|
||||||
: serverSocket.address.address;
|
join(homePath, geoFileName),
|
||||||
bool isSuccess = false;
|
|
||||||
if (Platform.isWindows && await system.checkIsAdmin()) {
|
|
||||||
isSuccess = await request.startCoreByHelper(arg);
|
|
||||||
}
|
|
||||||
if (isSuccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
process = await Process.start(
|
|
||||||
appPath.corePath,
|
|
||||||
[
|
|
||||||
arg,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
process!.stdout.listen((_) {});
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteSocketFile() async {
|
|
||||||
if (!Platform.isWindows) {
|
|
||||||
final file = File(unixSocketPath);
|
|
||||||
if (await file.exists()) {
|
|
||||||
await file.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_destroySocket() async {
|
|
||||||
if (socketCompleter.isCompleted) {
|
|
||||||
final lastSocket = await socketCompleter.future;
|
|
||||||
await lastSocket.close();
|
|
||||||
socketCompleter = Completer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleAction(Action action) {
|
|
||||||
final completer = callbackCompleterMap[action.id];
|
|
||||||
switch (action.method) {
|
|
||||||
case ActionMethod.initClash:
|
|
||||||
case ActionMethod.shutdown:
|
|
||||||
case ActionMethod.getIsInit:
|
|
||||||
case ActionMethod.startListener:
|
|
||||||
case ActionMethod.resetTraffic:
|
|
||||||
case ActionMethod.closeConnections:
|
|
||||||
case ActionMethod.closeConnection:
|
|
||||||
case ActionMethod.stopListener:
|
|
||||||
completer?.complete(action.data as bool);
|
|
||||||
return;
|
|
||||||
case ActionMethod.changeProxy:
|
|
||||||
case ActionMethod.getProxies:
|
|
||||||
case ActionMethod.getTraffic:
|
|
||||||
case ActionMethod.getTotalTraffic:
|
|
||||||
case ActionMethod.asyncTestDelay:
|
|
||||||
case ActionMethod.getConnections:
|
|
||||||
case ActionMethod.getExternalProviders:
|
|
||||||
case ActionMethod.getExternalProvider:
|
|
||||||
case ActionMethod.validateConfig:
|
|
||||||
case ActionMethod.updateConfig:
|
|
||||||
case ActionMethod.updateGeoData:
|
|
||||||
case ActionMethod.updateExternalProvider:
|
|
||||||
case ActionMethod.sideLoadExternalProvider:
|
|
||||||
case ActionMethod.getCountryCode:
|
|
||||||
case ActionMethod.getMemory:
|
|
||||||
completer?.complete(action.data as String);
|
|
||||||
return;
|
|
||||||
case ActionMethod.message:
|
|
||||||
clashMessage.controller.add(action.data as String);
|
|
||||||
return;
|
|
||||||
case ActionMethod.forceGc:
|
|
||||||
case ActionMethod.startLog:
|
|
||||||
case ActionMethod.stopLog:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T> _invoke<T>({
|
|
||||||
required ActionMethod method,
|
|
||||||
dynamic data,
|
|
||||||
Duration? timeout,
|
|
||||||
FutureOr<T> Function()? onTimeout,
|
|
||||||
}) async {
|
|
||||||
final id = "${method.name}#${other.id}";
|
|
||||||
final socket = await socketCompleter.future;
|
|
||||||
callbackCompleterMap[id] = Completer<T>();
|
|
||||||
socket.writeln(
|
|
||||||
json.encode(
|
|
||||||
Action(
|
|
||||||
id: id,
|
|
||||||
method: method,
|
|
||||||
data: data,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
|
|
||||||
timeout: timeout,
|
|
||||||
onLast: () {
|
|
||||||
callbackCompleterMap.remove(id);
|
|
||||||
},
|
|
||||||
onTimeout: onTimeout ??
|
|
||||||
() {
|
|
||||||
if (T is String) {
|
|
||||||
return "" as T;
|
|
||||||
}
|
|
||||||
if (T is bool) {
|
|
||||||
return false as T;
|
|
||||||
}
|
|
||||||
return null as T;
|
|
||||||
},
|
|
||||||
functionName: id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_prueInvoke({
|
|
||||||
required ActionMethod method,
|
|
||||||
dynamic data,
|
|
||||||
}) async {
|
|
||||||
final id = "${method.name}#${other.id}";
|
|
||||||
final socket = await socketCompleter.future;
|
|
||||||
socket.writeln(
|
|
||||||
json.encode(
|
|
||||||
Action(
|
|
||||||
id: id,
|
|
||||||
method: method,
|
|
||||||
data: data,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> init(String homeDir) {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.initClash,
|
|
||||||
data: homeDir,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
shutdown() async {
|
|
||||||
await _invoke<bool>(
|
|
||||||
method: ActionMethod.shutdown,
|
|
||||||
);
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
await request.stopCoreByHelper();
|
|
||||||
}
|
|
||||||
await _destroySocket();
|
|
||||||
process?.kill();
|
|
||||||
process = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> get isInit {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.getIsInit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
forceGc() {
|
|
||||||
_prueInvoke(method: ActionMethod.forceGc);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> validateConfig(String data) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.validateConfig,
|
|
||||||
data: data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
|
||||||
return await _invoke<String>(
|
|
||||||
method: ActionMethod.updateConfig,
|
|
||||||
data: json.encode(updateConfigParams),
|
|
||||||
timeout: const Duration(seconds: 20),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> getProxies() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getProxies,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.changeProxy,
|
|
||||||
data: json.encode(changeProxyParams),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getExternalProviders() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getExternalProviders,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getExternalProvider,
|
|
||||||
data: externalProviderName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateGeoData({
|
|
||||||
required String geoType,
|
|
||||||
required String geoName,
|
|
||||||
}) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.updateGeoData,
|
|
||||||
data: json.encode(
|
|
||||||
{
|
|
||||||
"geoType": geoType,
|
|
||||||
"geoName": geoName,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> sideLoadExternalProvider({
|
|
||||||
required String providerName,
|
|
||||||
required String data,
|
|
||||||
}) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.sideLoadExternalProvider,
|
|
||||||
data: json.encode({
|
|
||||||
"providerName": providerName,
|
|
||||||
"data": data,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> updateExternalProvider(String providerName) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.updateExternalProvider,
|
|
||||||
data: providerName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getConnections() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getConnections,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> closeConnections() {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.closeConnections,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> closeConnection(String id) {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.closeConnection,
|
|
||||||
data: id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getTotalTraffic(bool value) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getTotalTraffic,
|
|
||||||
data: value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getTraffic(bool value) {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getTraffic,
|
|
||||||
data: value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
resetTraffic() {
|
|
||||||
_prueInvoke(method: ActionMethod.resetTraffic);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
startLog() {
|
|
||||||
_prueInvoke(method: ActionMethod.startLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopLog() {
|
|
||||||
_prueInvoke(method: ActionMethod.stopLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> startListener() {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.startListener,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
stopListener() {
|
|
||||||
return _invoke<bool>(
|
|
||||||
method: ActionMethod.stopListener,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> asyncTestDelay(String proxyName) {
|
|
||||||
final delayParams = {
|
|
||||||
"proxy-name": proxyName,
|
|
||||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
|
||||||
};
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.asyncTestDelay,
|
|
||||||
data: json.encode(delayParams),
|
|
||||||
timeout: Duration(
|
|
||||||
milliseconds: 6000,
|
|
||||||
),
|
|
||||||
onTimeout: () {
|
|
||||||
return json.encode(
|
|
||||||
Delay(
|
|
||||||
name: proxyName,
|
|
||||||
value: -1,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
final isExists = await geoFile.exists();
|
||||||
);
|
if (isExists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final data = await rootBundle.load('assets/data/$geoFileName');
|
||||||
|
List<int> bytes = data.buffer.asUint8List();
|
||||||
|
await geoFile.writeAsBytes(bytes, flush: true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("$e");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() async {
|
Future<bool> init({
|
||||||
final server = await serverCompleter.future;
|
required ClashConfig clashConfig,
|
||||||
await server.close();
|
required Config config,
|
||||||
await _deleteSocketFile();
|
}) async {
|
||||||
}
|
await initGeo();
|
||||||
|
final homeDirPath = await appPath.getHomeDirPath();
|
||||||
@override
|
final isInit = clashCore.init(homeDirPath);
|
||||||
FutureOr<String> getCountryCode(String ip) {
|
return isInit;
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getCountryCode,
|
|
||||||
data: ip,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<String> getMemory() {
|
|
||||||
return _invoke<String>(
|
|
||||||
method: ActionMethod.getMemory,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final clashService = system.isDesktop ? ClashService() : null;
|
final clashService = ClashService();
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension ColorExtension on Color {
|
extension ColorExtension on Color {
|
||||||
|
toLight() {
|
||||||
Color get toLight {
|
|
||||||
return withOpacity(0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get toLighter {
|
|
||||||
return withOpacity(0.6);
|
return withOpacity(0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color get toSoft {
|
toLighter() {
|
||||||
|
return withOpacity(0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
toSoft() {
|
||||||
return withOpacity(0.12);
|
return withOpacity(0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color get toLittle {
|
toLittle() {
|
||||||
return withOpacity(0.03);
|
return withOpacity(0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,39 +23,14 @@ extension ColorExtension on Color {
|
|||||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||||
return hslDark.toColor();
|
return hslDark.toColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
Color blendDarken(
|
|
||||||
BuildContext context, {
|
|
||||||
double factor = 0.1,
|
|
||||||
}) {
|
|
||||||
final brightness = Theme.of(context).brightness;
|
|
||||||
return Color.lerp(
|
|
||||||
this,
|
|
||||||
brightness == Brightness.dark ? Colors.white : Colors.black,
|
|
||||||
factor,
|
|
||||||
)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color blendLighten(
|
|
||||||
BuildContext context, {
|
|
||||||
double factor = 0.1,
|
|
||||||
}) {
|
|
||||||
final brightness = Theme.of(context).brightness;
|
|
||||||
return Color.lerp(
|
|
||||||
this,
|
|
||||||
brightness == Brightness.dark ? Colors.black : Colors.white,
|
|
||||||
factor,
|
|
||||||
)!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ColorSchemeExtension on ColorScheme {
|
extension ColorSchemeExtension on ColorScheme {
|
||||||
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
|
||||||
? copyWith(
|
? copyWith(
|
||||||
surface: Colors.black,
|
surface: Colors.black,
|
||||||
surfaceContainer: surfaceContainer.darken(
|
background: Colors.black,
|
||||||
0.05,
|
surfaceContainer: surfaceContainer.darken(0.05),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: this;
|
: this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
export 'android.dart';
|
|
||||||
export 'app_localizations.dart';
|
|
||||||
export 'color.dart';
|
|
||||||
export 'constant.dart';
|
|
||||||
export 'context.dart';
|
|
||||||
export 'datetime.dart';
|
|
||||||
export 'function.dart';
|
|
||||||
export 'future.dart';
|
|
||||||
export 'http.dart';
|
|
||||||
export 'icons.dart';
|
|
||||||
export 'iterable.dart';
|
|
||||||
export 'keyboard.dart';
|
|
||||||
export 'launch.dart';
|
|
||||||
export 'link.dart';
|
|
||||||
export 'list.dart';
|
|
||||||
export 'lock.dart';
|
|
||||||
export 'measure.dart';
|
|
||||||
export 'navigation.dart';
|
|
||||||
export 'navigator.dart';
|
|
||||||
export 'network.dart';
|
|
||||||
export 'num.dart';
|
|
||||||
export 'other.dart';
|
|
||||||
export 'package.dart';
|
|
||||||
export 'path.dart';
|
export 'path.dart';
|
||||||
export 'picker.dart';
|
|
||||||
export 'preferences.dart';
|
|
||||||
export 'protocol.dart';
|
|
||||||
export 'proxy.dart';
|
|
||||||
export 'request.dart';
|
export 'request.dart';
|
||||||
export 'scroll.dart';
|
export 'preferences.dart';
|
||||||
export 'string.dart';
|
export 'constant.dart';
|
||||||
export 'system.dart';
|
export 'proxy.dart';
|
||||||
export 'text.dart';
|
export 'other.dart';
|
||||||
export 'tray.dart';
|
export 'num.dart';
|
||||||
|
export 'navigation.dart';
|
||||||
export 'window.dart';
|
export 'window.dart';
|
||||||
|
export 'system.dart';
|
||||||
|
export 'picker.dart';
|
||||||
|
export 'android.dart';
|
||||||
|
export 'launch.dart';
|
||||||
|
export 'protocol.dart';
|
||||||
|
export 'datetime.dart';
|
||||||
|
export 'context.dart';
|
||||||
|
export 'link.dart';
|
||||||
|
export 'text.dart';
|
||||||
|
export 'color.dart';
|
||||||
|
export 'list.dart';
|
||||||
|
export 'string.dart';
|
||||||
|
export 'app_localizations.dart';
|
||||||
|
export 'function.dart';
|
||||||
|
export 'package.dart';
|
||||||
|
export 'measure.dart';
|
||||||
export 'windows.dart';
|
export 'windows.dart';
|
||||||
|
export 'iterable.dart';
|
||||||
|
export 'scroll.dart';
|
||||||
|
export 'icons.dart';
|
||||||
|
export 'http.dart';
|
||||||
|
export 'keyboard.dart';
|
||||||
|
export 'network.dart';
|
||||||
|
export 'font.dart';
|
||||||
@@ -1,28 +1,18 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'system.dart';
|
||||||
|
|
||||||
const appName = "FlClash";
|
const appName = "FlClash";
|
||||||
const appHelperService = "FlClashHelperService";
|
|
||||||
const coreName = "clash.meta";
|
const coreName = "clash.meta";
|
||||||
const packageName = "com.follow.clash";
|
const packageName = "com.follow.clash";
|
||||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
|
||||||
const helperPort = 47890;
|
|
||||||
const helperTag = "2024125";
|
|
||||||
const baseInfoEdgeInsets = EdgeInsets.symmetric(
|
|
||||||
vertical: 16,
|
|
||||||
horizontal: 16,
|
|
||||||
);
|
|
||||||
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
||||||
const moreDuration = Duration(milliseconds: 100);
|
const moreDuration = Duration(milliseconds: 100);
|
||||||
const animateDuration = Duration(milliseconds: 100);
|
const animateDuration = Duration(milliseconds: 100);
|
||||||
const commonDuration = Duration(milliseconds: 300);
|
|
||||||
const defaultUpdateDuration = Duration(days: 1);
|
const defaultUpdateDuration = Duration(days: 1);
|
||||||
const mmdbFileName = "geoip.metadb";
|
const mmdbFileName = "geoip.metadb";
|
||||||
const asnFileName = "ASN.mmdb";
|
const asnFileName = "ASN.mmdb";
|
||||||
@@ -31,7 +21,7 @@ const geoSiteFileName = "GeoSite.dat";
|
|||||||
final double kHeaderHeight = system.isDesktop
|
final double kHeaderHeight = system.isDesktop
|
||||||
? !Platform.isMacOS
|
? !Platform.isMacOS
|
||||||
? 40
|
? 40
|
||||||
: 28
|
: 26
|
||||||
: 0;
|
: 0;
|
||||||
const GeoXMap defaultGeoXMap = {
|
const GeoXMap defaultGeoXMap = {
|
||||||
"mmdb":
|
"mmdb":
|
||||||
@@ -84,7 +74,3 @@ const viewModeColumnsMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultPrimaryColor = Colors.brown;
|
const defaultPrimaryColor = Colors.brown;
|
||||||
|
|
||||||
double getWidgetHeight(num lines) {
|
|
||||||
return max(lines * 84 + (lines - 1) * 16, 0);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import 'package:fl_clash/manager/manager.dart';
|
|
||||||
import 'package:fl_clash/widgets/scaffold.dart';
|
import 'package:fl_clash/widgets/scaffold.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension BuildContextExtension on BuildContext {
|
extension BuildContextExtension on BuildContext {
|
||||||
|
|
||||||
CommonScaffoldState? get commonScaffoldState {
|
CommonScaffoldState? get commonScaffoldState {
|
||||||
return findAncestorStateOfType<CommonScaffoldState>();
|
return findAncestorStateOfType<CommonScaffoldState>();
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotifier(String text) {
|
Size get appSize{
|
||||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
Size get appSize {
|
|
||||||
return MediaQuery.of(this).size;
|
return MediaQuery.of(this).size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ class DAVClient {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
client.setConnectTimeout(8000);
|
client.setConnectTimeout(8000);
|
||||||
client.setSendTimeout(60000);
|
client.setSendTimeout(8000);
|
||||||
client.setReceiveTimeout(60000);
|
client.setReceiveTimeout(8000);
|
||||||
pingCompleter.complete(_ping());
|
pingCompleter.complete(_ping());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
lib/common/font.dart
Normal file
3
lib/common/font.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class Fonts{
|
||||||
|
static String get twEmoji => "Twemoji";
|
||||||
|
}
|
||||||
@@ -1,33 +1,26 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class Debouncer {
|
class Debouncer {
|
||||||
Map<dynamic, Timer> operators = {};
|
final Duration delay;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
call(
|
Debouncer({required this.delay});
|
||||||
dynamic tag,
|
|
||||||
Function func, {
|
|
||||||
List<dynamic>? args,
|
|
||||||
Duration duration = const Duration(milliseconds: 600),
|
|
||||||
}) {
|
|
||||||
final timer = operators[tag];
|
|
||||||
if (timer != null) {
|
|
||||||
timer.cancel();
|
|
||||||
}
|
|
||||||
operators[tag] = Timer(
|
|
||||||
duration,
|
|
||||||
() {
|
|
||||||
operators.remove(tag);
|
|
||||||
Function.apply(
|
|
||||||
func,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(dynamic tag) {
|
void call(Function action, List<dynamic> positionalArguments, [Map<Symbol, dynamic>? namedArguments]) {
|
||||||
operators[tag]?.cancel();
|
_timer?.cancel();
|
||||||
|
_timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final debouncer = Debouncer();
|
Function debounce<F extends Function>(F func,{int milliseconds = 600}) {
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
return ([List<dynamic>? args, Map<Symbol, dynamic>? namedArgs]) {
|
||||||
|
if (timer != null) {
|
||||||
|
timer!.cancel();
|
||||||
|
}
|
||||||
|
timer = Timer(Duration(milliseconds: milliseconds), () async {
|
||||||
|
await Function.apply(func, args ?? [], namedArgs);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
extension CompleterExt<T> on Completer<T> {
|
|
||||||
safeFuture({
|
|
||||||
Duration? timeout,
|
|
||||||
VoidCallback? onLast,
|
|
||||||
FutureOr<T> Function()? onTimeout,
|
|
||||||
required String functionName,
|
|
||||||
}) {
|
|
||||||
final realTimeout = timeout ?? const Duration(seconds: 6);
|
|
||||||
Timer(realTimeout + Duration(milliseconds: 1000), () {
|
|
||||||
if (onLast != null) {
|
|
||||||
onLast();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return future.withTimeout(
|
|
||||||
timeout: realTimeout,
|
|
||||||
functionName: functionName,
|
|
||||||
onTimeout: onTimeout,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FutureExt<T> on Future<T> {
|
|
||||||
Future<T> withTimeout({
|
|
||||||
required Duration timeout,
|
|
||||||
required String functionName,
|
|
||||||
FutureOr<T> Function()? onTimeout,
|
|
||||||
}) {
|
|
||||||
return this.timeout(
|
|
||||||
timeout,
|
|
||||||
onTimeout: () async {
|
|
||||||
if (onTimeout != null) {
|
|
||||||
return onTimeout();
|
|
||||||
} else {
|
|
||||||
throw TimeoutException('$functionName timeout');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'constant.dart';
|
|
||||||
|
|
||||||
class FlClashHttpOverrides extends HttpOverrides {
|
class FlClashHttpOverrides extends HttpOverrides {
|
||||||
@override
|
@override
|
||||||
@@ -11,13 +10,10 @@ class FlClashHttpOverrides extends HttpOverrides {
|
|||||||
final client = super.createHttpClient(context);
|
final client = super.createHttpClient(context);
|
||||||
client.badCertificateCallback = (_, __, ___) => true;
|
client.badCertificateCallback = (_, __, ___) => true;
|
||||||
client.findProxy = (url) {
|
client.findProxy = (url) {
|
||||||
if ([localhost].contains(url.host)) {
|
debugPrint("find $url");
|
||||||
return "DIRECT";
|
|
||||||
}
|
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final port = appController.clashConfig.mixedPort;
|
final port = appController.clashConfig.mixedPort;
|
||||||
final isStart = appController.appFlowingState.isStart;
|
final isStart = appController.appFlowingState.isStart;
|
||||||
debugPrint("find $url proxy:$isStart");
|
|
||||||
if (!isStart) return "DIRECT";
|
if (!isStart) return "DIRECT";
|
||||||
return "PROXY localhost:$port";
|
return "PROXY localhost:$port";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:fl_clash/models/models.dart' hide Process;
|
||||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||||
|
|
||||||
import 'constant.dart';
|
import 'constant.dart';
|
||||||
import 'system.dart';
|
import 'system.dart';
|
||||||
|
import 'windows.dart';
|
||||||
|
|
||||||
class AutoLaunch {
|
class AutoLaunch {
|
||||||
static AutoLaunch? _instance;
|
static AutoLaunch? _instance;
|
||||||
@@ -25,15 +26,60 @@ class AutoLaunch {
|
|||||||
return await launchAtStartup.isEnabled();
|
return await launchAtStartup.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> get windowsIsEnable async {
|
||||||
|
final res = await Process.run(
|
||||||
|
'schtasks',
|
||||||
|
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
|
||||||
|
runInShell: true,
|
||||||
|
);
|
||||||
|
return res.stdout.toString().contains(Platform.resolvedExecutable);
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> enable() async {
|
Future<bool> enable() async {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
await windowsDisable();
|
||||||
|
}
|
||||||
return await launchAtStartup.enable();
|
return await launchAtStartup.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
windowsDisable() async {
|
||||||
|
final res = await Process.run(
|
||||||
|
'schtasks',
|
||||||
|
[
|
||||||
|
'/Delete',
|
||||||
|
'/TN',
|
||||||
|
appName,
|
||||||
|
'/F',
|
||||||
|
],
|
||||||
|
runInShell: true,
|
||||||
|
);
|
||||||
|
return res.exitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> windowsEnable() async {
|
||||||
|
await disable();
|
||||||
|
return await windows?.registerTask(appName) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> disable() async {
|
Future<bool> disable() async {
|
||||||
return await launchAtStartup.disable();
|
return await launchAtStartup.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(bool isAutoLaunch) async {
|
updateStatus(AutoLaunchState state) async {
|
||||||
|
final isAdminAutoLaunch = state.isAdminAutoLaunch;
|
||||||
|
final isAutoLaunch = state.isAutoLaunch;
|
||||||
|
if (Platform.isWindows && isAdminAutoLaunch) {
|
||||||
|
if (await windowsIsEnable == isAutoLaunch) return;
|
||||||
|
if (isAutoLaunch) {
|
||||||
|
final isEnable = await windowsEnable();
|
||||||
|
if (!isEnable) {
|
||||||
|
enable();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
windowsDisable();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (await isEnable == isAutoLaunch) return;
|
if (await isEnable == isAutoLaunch) return;
|
||||||
if (isAutoLaunch == true) {
|
if (isAutoLaunch == true) {
|
||||||
enable();
|
enable();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
typedef InstallConfigCallBack = void Function(String url);
|
typedef InstallConfigCallBack = void Function(String url);
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class LinkManager {
|
|||||||
initAppLinksListen(installConfigCallBack) async {
|
initAppLinksListen(installConfigCallBack) async {
|
||||||
debugPrint("initAppLinksListen");
|
debugPrint("initAppLinksListen");
|
||||||
destroy();
|
destroy();
|
||||||
subscription = _appLinks.uriLinkStream.listen(
|
subscription = _appLinks.allUriLinkStream.listen(
|
||||||
(uri) {
|
(uri) {
|
||||||
debugPrint('onAppLink: $uri');
|
debugPrint('onAppLink: $uri');
|
||||||
if (uri.host == 'install-config') {
|
if (uri.host == 'install-config') {
|
||||||
@@ -31,7 +31,8 @@ class LinkManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
|
||||||
|
destroy(){
|
||||||
if (subscription != null) {
|
if (subscription != null) {
|
||||||
subscription?.cancel();
|
subscription?.cancel();
|
||||||
subscription = null;
|
subscription = null;
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
|
|
||||||
class SingleInstanceLock {
|
|
||||||
static SingleInstanceLock? _instance;
|
|
||||||
RandomAccessFile? _accessFile;
|
|
||||||
|
|
||||||
SingleInstanceLock._internal();
|
|
||||||
|
|
||||||
factory SingleInstanceLock() {
|
|
||||||
_instance ??= SingleInstanceLock._internal();
|
|
||||||
return _instance!;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> acquire() async {
|
|
||||||
try {
|
|
||||||
final lockFilePath = await appPath.getLockFilePath();
|
|
||||||
final lockFile = File(lockFilePath);
|
|
||||||
await lockFile.create();
|
|
||||||
_accessFile = await lockFile.open(mode: FileMode.write);
|
|
||||||
await _accessFile?.lock();
|
|
||||||
return true;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final singleInstanceLock = SingleInstanceLock();
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class BaseNavigator {
|
|
||||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
|
||||||
return await Navigator.of(context).push<T>(
|
|
||||||
CommonRoute(
|
|
||||||
builder: (context) => child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CommonRoute<T> extends MaterialPageRoute<T> {
|
|
||||||
CommonRoute({
|
|
||||||
required super.builder,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
|
||||||
begin: const Offset(1.0, 0.0),
|
|
||||||
end: Offset.zero,
|
|
||||||
);
|
|
||||||
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
|
|
||||||
begin: Offset.zero,
|
|
||||||
end: const Offset(-1.0 / 3.0, 0.0),
|
|
||||||
);
|
|
||||||
|
|
||||||
class CommonPageTransitionsBuilder extends PageTransitionsBuilder {
|
|
||||||
const CommonPageTransitionsBuilder();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildTransitions<T>(
|
|
||||||
PageRoute<T> route,
|
|
||||||
BuildContext context,
|
|
||||||
Animation<double> animation,
|
|
||||||
Animation<double> secondaryAnimation,
|
|
||||||
Widget child,
|
|
||||||
) {
|
|
||||||
return CommonPageTransition(
|
|
||||||
context: context,
|
|
||||||
primaryRouteAnimation: animation,
|
|
||||||
secondaryRouteAnimation: secondaryAnimation,
|
|
||||||
linearTransition: false,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CommonPageTransition extends StatefulWidget {
|
|
||||||
const CommonPageTransition({
|
|
||||||
super.key,
|
|
||||||
required this.context,
|
|
||||||
required this.primaryRouteAnimation,
|
|
||||||
required this.secondaryRouteAnimation,
|
|
||||||
required this.child,
|
|
||||||
required this.linearTransition,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
final Animation<double> primaryRouteAnimation;
|
|
||||||
|
|
||||||
final Animation<double> secondaryRouteAnimation;
|
|
||||||
|
|
||||||
final BuildContext context;
|
|
||||||
|
|
||||||
final bool linearTransition;
|
|
||||||
|
|
||||||
static Widget? delegatedTransition(
|
|
||||||
BuildContext context,
|
|
||||||
Animation<double> animation,
|
|
||||||
Animation<double> secondaryAnimation,
|
|
||||||
bool allowSnapshotting,
|
|
||||||
Widget? child) {
|
|
||||||
final Animation<Offset> delegatedPositionAnimation = CurvedAnimation(
|
|
||||||
parent: secondaryAnimation,
|
|
||||||
curve: Curves.linearToEaseOut,
|
|
||||||
reverseCurve: Curves.easeInToLinear,
|
|
||||||
).drive(_kMiddleLeftTween);
|
|
||||||
|
|
||||||
assert(debugCheckHasDirectionality(context));
|
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
|
||||||
return SlideTransition(
|
|
||||||
position: delegatedPositionAnimation,
|
|
||||||
textDirection: textDirection,
|
|
||||||
transformHitTests: false,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CommonPageTransition> createState() => _CommonPageTransitionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CommonPageTransitionState extends State<CommonPageTransition> {
|
|
||||||
late Animation<Offset> _primaryPositionAnimation;
|
|
||||||
late Animation<Offset> _secondaryPositionAnimation;
|
|
||||||
late Animation<Decoration> _primaryShadowAnimation;
|
|
||||||
CurvedAnimation? _primaryPositionCurve;
|
|
||||||
CurvedAnimation? _secondaryPositionCurve;
|
|
||||||
CurvedAnimation? _primaryShadowCurve;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_setupAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant CommonPageTransition oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
|
|
||||||
oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
|
|
||||||
oldWidget.linearTransition != widget.linearTransition) {
|
|
||||||
_disposeCurve();
|
|
||||||
_setupAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_disposeCurve();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _disposeCurve() {
|
|
||||||
_primaryPositionCurve?.dispose();
|
|
||||||
_secondaryPositionCurve?.dispose();
|
|
||||||
_primaryShadowCurve?.dispose();
|
|
||||||
_primaryPositionCurve = null;
|
|
||||||
_secondaryPositionCurve = null;
|
|
||||||
_primaryShadowCurve = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setupAnimation() {
|
|
||||||
if (!widget.linearTransition) {
|
|
||||||
_primaryPositionCurve = CurvedAnimation(
|
|
||||||
parent: widget.primaryRouteAnimation,
|
|
||||||
curve: Curves.fastEaseInToSlowEaseOut,
|
|
||||||
reverseCurve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
_secondaryPositionCurve = CurvedAnimation(
|
|
||||||
parent: widget.secondaryRouteAnimation,
|
|
||||||
curve: Curves.linearToEaseOut,
|
|
||||||
reverseCurve: Curves.easeInToLinear,
|
|
||||||
);
|
|
||||||
_primaryShadowCurve = CurvedAnimation(
|
|
||||||
parent: widget.primaryRouteAnimation,
|
|
||||||
curve: Curves.linearToEaseOut,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_primaryPositionAnimation =
|
|
||||||
(_primaryPositionCurve ?? widget.primaryRouteAnimation)
|
|
||||||
.drive(_kRightMiddleTween);
|
|
||||||
_secondaryPositionAnimation =
|
|
||||||
(_secondaryPositionCurve ?? widget.secondaryRouteAnimation)
|
|
||||||
.drive(_kMiddleLeftTween);
|
|
||||||
_primaryShadowAnimation =
|
|
||||||
(_primaryShadowCurve ?? widget.primaryRouteAnimation).drive(
|
|
||||||
DecorationTween(
|
|
||||||
begin: const _CommonEdgeShadowDecoration(),
|
|
||||||
end: _CommonEdgeShadowDecoration(
|
|
||||||
<Color>[
|
|
||||||
widget.context.colorScheme.inverseSurface.withOpacity(
|
|
||||||
0.06,
|
|
||||||
),
|
|
||||||
Colors.transparent,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
assert(debugCheckHasDirectionality(context));
|
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
|
||||||
return SlideTransition(
|
|
||||||
position: _secondaryPositionAnimation,
|
|
||||||
textDirection: textDirection,
|
|
||||||
transformHitTests: false,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: _primaryPositionAnimation,
|
|
||||||
textDirection: textDirection,
|
|
||||||
child: DecoratedBoxTransition(
|
|
||||||
decoration: _primaryShadowAnimation,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CommonEdgeShadowDecoration extends Decoration {
|
|
||||||
final List<Color>? _colors;
|
|
||||||
|
|
||||||
const _CommonEdgeShadowDecoration([this._colors]);
|
|
||||||
|
|
||||||
@override
|
|
||||||
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
|
|
||||||
return _CommonEdgeShadowPainter(this, onChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CommonEdgeShadowPainter extends BoxPainter {
|
|
||||||
_CommonEdgeShadowPainter(
|
|
||||||
this._decoration,
|
|
||||||
super.onChanged,
|
|
||||||
) : assert(_decoration._colors == null || _decoration._colors!.length > 1);
|
|
||||||
|
|
||||||
final _CommonEdgeShadowDecoration _decoration;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
|
|
||||||
final List<Color>? colors = _decoration._colors;
|
|
||||||
if (colors == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final double shadowWidth = 0.05 * configuration.size!.width;
|
|
||||||
final double shadowHeight = configuration.size!.height;
|
|
||||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
|
||||||
|
|
||||||
final TextDirection? textDirection = configuration.textDirection;
|
|
||||||
assert(textDirection != null);
|
|
||||||
final (double shadowDirection, double start) = switch (textDirection!) {
|
|
||||||
TextDirection.rtl => (1, offset.dx + configuration.size!.width),
|
|
||||||
TextDirection.ltr => (-1, offset.dx),
|
|
||||||
};
|
|
||||||
|
|
||||||
int bandColorIndex = 0;
|
|
||||||
for (int dx = 0; dx < shadowWidth; dx += 1) {
|
|
||||||
if (dx ~/ bandWidth != bandColorIndex) {
|
|
||||||
bandColorIndex += 1;
|
|
||||||
}
|
|
||||||
final Paint paint = Paint()
|
|
||||||
..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1],
|
|
||||||
(dx % bandWidth) / bandWidth)!;
|
|
||||||
final double x = start + shadowDirection * dx;
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
extension NumExtension on num {
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
extension NumExt on num {
|
|
||||||
String fixed({digit = 2}) {
|
String fixed({digit = 2}) {
|
||||||
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DoubleExt on double {
|
|
||||||
moreOrEqual(double value) {
|
|
||||||
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension OffsetExt on Offset {
|
|
||||||
double getCrossAxisOffset(Axis direction) {
|
|
||||||
return direction == Axis.vertical ? dx : dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getMainAxisOffset(Axis direction) {
|
|
||||||
return direction == Axis.vertical ? dy : dx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool less(Offset offset) {
|
|
||||||
if (dy < offset.dy) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (dy == offset.dy && dx < offset.dx) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension RectExt on Rect {
|
|
||||||
doRectIntersect(Rect rect) {
|
|
||||||
return left < rect.right &&
|
|
||||||
right > rect.left &&
|
|
||||||
top < rect.bottom &&
|
|
||||||
bottom > rect.top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import 'dart:io';
|
|||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:lpinyin/lpinyin.dart';
|
import 'package:lpinyin/lpinyin.dart';
|
||||||
import 'package:zxing2/qrcode.dart';
|
import 'package:zxing2/qrcode.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
class Other {
|
class Other {
|
||||||
Color? getDelayColor(int? delay) {
|
Color? getDelayColor(int? delay) {
|
||||||
@@ -19,14 +18,6 @@ class Other {
|
|||||||
return const Color(0xFFC57F0A);
|
return const Color(0xFFC57F0A);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get id {
|
|
||||||
final timestamp = DateTime.now().microsecondsSinceEpoch;
|
|
||||||
final random = Random();
|
|
||||||
final randomStr =
|
|
||||||
String.fromCharCodes(List.generate(8, (_) => random.nextInt(26) + 97));
|
|
||||||
return "$timestamp$randomStr";
|
|
||||||
}
|
|
||||||
|
|
||||||
String getDateStringLast2(int value) {
|
String getDateStringLast2(int value) {
|
||||||
var valueRaw = "0$value";
|
var valueRaw = "0$value";
|
||||||
return valueRaw.substring(
|
return valueRaw.substring(
|
||||||
@@ -34,19 +25,6 @@ class Other {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get uuidV4 {
|
|
||||||
final Random random = Random();
|
|
||||||
final bytes = List.generate(16, (_) => random.nextInt(256));
|
|
||||||
|
|
||||||
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
|
||||||
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
|
||||||
|
|
||||||
final hex =
|
|
||||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTimeDifference(DateTime dateTime) {
|
String getTimeDifference(DateTime dateTime) {
|
||||||
var currentDateTime = DateTime.now();
|
var currentDateTime = DateTime.now();
|
||||||
var difference = currentDateTime.difference(dateTime);
|
var difference = currentDateTime.difference(dateTime);
|
||||||
@@ -123,17 +101,20 @@ class Other {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getTrayIconPath({
|
String getTrayIconPath({
|
||||||
|
required bool isStart,
|
||||||
required Brightness brightness,
|
required Brightness brightness,
|
||||||
}) {
|
}) {
|
||||||
if (Platform.isMacOS) {
|
if(Platform.isMacOS){
|
||||||
return "assets/images/icon_white.png";
|
return "assets/images/icon_white.png";
|
||||||
}
|
}
|
||||||
final suffix = Platform.isWindows ? "ico" : "png";
|
final suffix = Platform.isWindows ? "ico" : "png";
|
||||||
return "assets/images/icon.$suffix";
|
if (isStart && Platform.isWindows) {
|
||||||
// return switch (brightness) {
|
return "assets/images/icon.$suffix";
|
||||||
// Brightness.dark => "assets/images/icon_white.$suffix",
|
}
|
||||||
// Brightness.light => "assets/images/icon_black.$suffix",
|
return switch (brightness) {
|
||||||
// };
|
Brightness.dark => "assets/images/icon_white.$suffix",
|
||||||
|
Brightness.light => "assets/images/icon_black.$suffix",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
int compareVersions(String version1, String version2) {
|
int compareVersions(String version1, String version2) {
|
||||||
@@ -207,8 +188,10 @@ class Other {
|
|||||||
return parameters[fileNameKey];
|
return parameters[fileNameKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
FlutterView getScreen() {
|
double getViewWidth() {
|
||||||
return WidgetsBinding.instance.platformDispatcher.views.first;
|
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||||
|
final size = view.physicalSize / view.devicePixelRatio;
|
||||||
|
return size.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> parseReleaseBody(String? body) {
|
List<String> parseReleaseBody(String? body) {
|
||||||
@@ -238,7 +221,7 @@ class Other {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int getProfilesColumns(double viewWidth) {
|
int getProfilesColumns(double viewWidth) {
|
||||||
return max((viewWidth / 350).floor(), 1);
|
return max((viewWidth / 400).floor(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getBackupFileName() {
|
String getBackupFileName() {
|
||||||
@@ -253,32 +236,6 @@ class Other {
|
|||||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||||
return view.physicalSize / view.devicePixelRatio;
|
return view.physicalSize / view.devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getLocalIpAddress() async {
|
|
||||||
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
|
||||||
includeLoopback: false,
|
|
||||||
)
|
|
||||||
..sort((a, b) {
|
|
||||||
if (a.isWifi && !b.isWifi) return -1;
|
|
||||||
if (!a.isWifi && b.isWifi) return 1;
|
|
||||||
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
|
||||||
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
for (final interface in interfaces) {
|
|
||||||
final addresses = interface.addresses;
|
|
||||||
if (addresses.isEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
addresses.sort((a, b) {
|
|
||||||
if (a.isIPv4 && !b.isIPv4) return -1;
|
|
||||||
if (!a.isIPv4 && b.isIPv4) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
return addresses.first.address;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final other = Other();
|
final other = Other();
|
||||||
|
|||||||
@@ -13,17 +13,34 @@ class AppPath {
|
|||||||
Completer<Directory> tempDir = Completer();
|
Completer<Directory> tempDir = Completer();
|
||||||
late String appDirPath;
|
late String appDirPath;
|
||||||
|
|
||||||
|
// Future<Directory> _createDesktopCacheDir() async {
|
||||||
|
// final dir = Directory(path);
|
||||||
|
// if (await dir.exists()) {
|
||||||
|
// await dir.create(recursive: true);
|
||||||
|
// }
|
||||||
|
// return dir;
|
||||||
|
// }
|
||||||
|
|
||||||
AppPath._internal() {
|
AppPath._internal() {
|
||||||
appDirPath = join(dirname(Platform.resolvedExecutable));
|
appDirPath = join(dirname(Platform.resolvedExecutable));
|
||||||
getApplicationSupportDirectory().then((value) {
|
getApplicationSupportDirectory().then((value) {
|
||||||
dataDir.complete(value);
|
dataDir.complete(value);
|
||||||
});
|
});
|
||||||
getTemporaryDirectory().then((value) {
|
getTemporaryDirectory().then((value){
|
||||||
tempDir.complete(value);
|
tempDir.complete(value);
|
||||||
});
|
});
|
||||||
getDownloadsDirectory().then((value) {
|
getDownloadsDirectory().then((value) {
|
||||||
downloadDir.complete(value);
|
downloadDir.complete(value);
|
||||||
});
|
});
|
||||||
|
// if (Platform.isAndroid) {
|
||||||
|
// getApplicationSupportDirectory().then((value) {
|
||||||
|
// cacheDir.complete(value);
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// _createDesktopCacheDir().then((value) {
|
||||||
|
// cacheDir.complete(value);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AppPath() {
|
factory AppPath() {
|
||||||
@@ -31,23 +48,6 @@ class AppPath {
|
|||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get executableExtension {
|
|
||||||
return Platform.isWindows ? ".exe" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String get executableDirPath {
|
|
||||||
final currentExecutablePath = Platform.resolvedExecutable;
|
|
||||||
return dirname(currentExecutablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
String get corePath {
|
|
||||||
return join(executableDirPath, "FlClashCore$executableExtension");
|
|
||||||
}
|
|
||||||
|
|
||||||
String get helperPath {
|
|
||||||
return join(executableDirPath, "$appHelperService$executableExtension");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> getDownloadDirPath() async {
|
Future<String> getDownloadDirPath() async {
|
||||||
final directory = await downloadDir.future;
|
final directory = await downloadDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
@@ -58,11 +58,6 @@ class AppPath {
|
|||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getLockFilePath() async {
|
|
||||||
final directory = await dataDir.future;
|
|
||||||
return join(directory.path, "FlClash.lock");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> getProfilesPath() async {
|
Future<String> getProfilesPath() async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return join(directory.path, profilesDirectoryName);
|
return join(directory.path, profilesDirectoryName);
|
||||||
@@ -74,12 +69,6 @@ class AppPath {
|
|||||||
return join(directory, "$id.yaml");
|
return join(directory, "$id.yaml");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getProvidersPath(String? id) async {
|
|
||||||
if (id == null) return null;
|
|
||||||
final directory = await getProfilesPath();
|
|
||||||
return join(directory, "providers", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> get tempPath async {
|
Future<String> get tempPath async {
|
||||||
final directory = await tempDir.future;
|
final directory = await tempDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.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/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
@@ -71,113 +68,36 @@ class Request {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> _ipInfoSources = [
|
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||||
"https://ipwho.is/?fields=ip&output=csv",
|
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||||
"https://ipinfo.io/ip",
|
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||||
"https://ifconfig.me/ip/",
|
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||||
];
|
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||||
|
};
|
||||||
|
|
||||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||||
for (final source in _ipInfoSources) {
|
for (final source in _ipInfoSources.entries) {
|
||||||
try {
|
try {
|
||||||
final response = await _dio
|
final response = await _dio
|
||||||
.get<String>(
|
.get<Map<String, dynamic>>(
|
||||||
source,
|
source.key,
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
)
|
)
|
||||||
.timeout(httpTimeoutDuration);
|
.timeout(
|
||||||
if (response.statusCode != 200 || response.data == null) {
|
httpTimeoutDuration,
|
||||||
continue;
|
);
|
||||||
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
|
return source.value(response.data!);
|
||||||
}
|
}
|
||||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
|
||||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return ipInfo;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("checkIp error ===> $e");
|
if (cancelToken?.isCancelled == true) {
|
||||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
|
||||||
throw "cancelled";
|
throw "cancelled";
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> pingHelper() async {
|
|
||||||
try {
|
|
||||||
final response = await _dio
|
|
||||||
.get(
|
|
||||||
"http://$localhost:$helperPort/ping",
|
|
||||||
options: Options(
|
|
||||||
responseType: ResponseType.plain,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.timeout(
|
|
||||||
const Duration(
|
|
||||||
milliseconds: 2000,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (response.data as String) == helperTag;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> startCoreByHelper(String arg) async {
|
|
||||||
try {
|
|
||||||
final response = await _dio
|
|
||||||
.post(
|
|
||||||
"http://$localhost:$helperPort/start",
|
|
||||||
data: json.encode({
|
|
||||||
"path": appPath.corePath,
|
|
||||||
"arg": arg,
|
|
||||||
}),
|
|
||||||
options: Options(
|
|
||||||
responseType: ResponseType.plain,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.timeout(
|
|
||||||
const Duration(
|
|
||||||
milliseconds: 2000,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final data = response.data as String;
|
|
||||||
return data.isEmpty;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> stopCoreByHelper() async {
|
|
||||||
try {
|
|
||||||
final response = await _dio
|
|
||||||
.post(
|
|
||||||
"http://$localhost:$helperPort/stop",
|
|
||||||
options: Options(
|
|
||||||
responseType: ResponseType.plain,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.timeout(
|
|
||||||
const Duration(
|
|
||||||
milliseconds: 2000,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final data = response.data as String;
|
|
||||||
return data.isEmpty;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final request = Request();
|
final request = Request();
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/input.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'window.dart';
|
||||||
|
|
||||||
class System {
|
class System {
|
||||||
static System? _instance;
|
static System? _instance;
|
||||||
|
|
||||||
@@ -21,6 +19,12 @@ class System {
|
|||||||
bool get isDesktop =>
|
bool get isDesktop =>
|
||||||
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||||
|
|
||||||
|
get isAdmin async {
|
||||||
|
if (!Platform.isWindows) return false;
|
||||||
|
final result = await Process.run('net', ['session'], runInShell: true);
|
||||||
|
return result.exitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> get version async {
|
Future<int> get version async {
|
||||||
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
|
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
|
||||||
return switch (Platform.operatingSystem) {
|
return switch (Platform.operatingSystem) {
|
||||||
@@ -31,73 +35,6 @@ class System {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> checkIsAdmin() async {
|
|
||||||
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
final result = await windows?.checkService();
|
|
||||||
return result == WindowsHelperServiceStatus.running;
|
|
||||||
} else if (Platform.isMacOS) {
|
|
||||||
final result = await Process.run('stat', ['-f', '%Su:%Sg %Sp', corePath]);
|
|
||||||
final output = result.stdout.trim();
|
|
||||||
if (output.startsWith('root:admin') && output.contains('rws')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else if (Platform.isLinux) {
|
|
||||||
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
|
|
||||||
final output = result.stdout.trim();
|
|
||||||
if (output.startsWith('root:') && output.contains('rwx')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<AuthorizeCode> authorizeCore() async {
|
|
||||||
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
|
|
||||||
final isAdmin = await checkIsAdmin();
|
|
||||||
if (isAdmin) {
|
|
||||||
return AuthorizeCode.none;
|
|
||||||
}
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
final result = await windows?.registerService();
|
|
||||||
if (result == true) {
|
|
||||||
return AuthorizeCode.success;
|
|
||||||
}
|
|
||||||
return AuthorizeCode.error;
|
|
||||||
} else if (Platform.isMacOS) {
|
|
||||||
final shell = 'chown root:admin $corePath; chmod +sx $corePath';
|
|
||||||
final arguments = [
|
|
||||||
"-e",
|
|
||||||
'do shell script "$shell" with administrator privileges',
|
|
||||||
];
|
|
||||||
final result = await Process.run("osascript", arguments);
|
|
||||||
if (result.exitCode != 0) {
|
|
||||||
return AuthorizeCode.error;
|
|
||||||
}
|
|
||||||
return AuthorizeCode.success;
|
|
||||||
} else if (Platform.isLinux) {
|
|
||||||
final shell = Platform.environment['SHELL'] ?? 'bash';
|
|
||||||
final password = await globalState.showCommonDialog<String>(
|
|
||||||
child: InputDialog(
|
|
||||||
title: appLocalizations.pleaseInputAdminPassword,
|
|
||||||
value: '',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final arguments = [
|
|
||||||
"-c",
|
|
||||||
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
|
|
||||||
];
|
|
||||||
final result = await Process.run(shell, arguments);
|
|
||||||
if (result.exitCode != 0) {
|
|
||||||
return AuthorizeCode.error;
|
|
||||||
}
|
|
||||||
return AuthorizeCode.success;
|
|
||||||
}
|
|
||||||
return AuthorizeCode.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
back() async {
|
back() async {
|
||||||
await app?.moveTaskToBack();
|
await app?.moveTaskToBack();
|
||||||
await window?.hide();
|
await window?.hide();
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'color.dart';
|
import 'color.dart';
|
||||||
|
|
||||||
extension TextStyleExtension on TextStyle {
|
extension TextStyleExtension on TextStyle {
|
||||||
TextStyle get toLight => copyWith(color: color?.toLight);
|
TextStyle get toLight => copyWith(color: color?.toLight());
|
||||||
|
|
||||||
TextStyle get toLighter => copyWith(color: color?.toLighter);
|
TextStyle get toLighter => copyWith(color: color?.toLighter());
|
||||||
|
|
||||||
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
|
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
|
||||||
|
|
||||||
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
|
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
|
||||||
|
|
||||||
TextStyle adjustSize(int size) => copyWith(
|
TextStyle get toMinus => copyWith(fontSize: fontSize! - 2);
|
||||||
fontSize: fontSize! + size,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
|
||||||
|
|
||||||
import 'app_localizations.dart';
|
|
||||||
import 'constant.dart';
|
|
||||||
import 'other.dart';
|
|
||||||
import 'window.dart';
|
|
||||||
|
|
||||||
class Tray {
|
|
||||||
Future _updateSystemTray({
|
|
||||||
required Brightness? brightness,
|
|
||||||
bool force = false,
|
|
||||||
}) async {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Platform.isLinux || force) {
|
|
||||||
await trayManager.destroy();
|
|
||||||
}
|
|
||||||
await trayManager.setIcon(
|
|
||||||
other.getTrayIconPath(
|
|
||||||
brightness: brightness ??
|
|
||||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
|
||||||
),
|
|
||||||
isTemplate: true,
|
|
||||||
);
|
|
||||||
if (!Platform.isLinux) {
|
|
||||||
await trayManager.setToolTip(
|
|
||||||
appName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update({
|
|
||||||
required AppState appState,
|
|
||||||
required AppFlowingState appFlowingState,
|
|
||||||
required Config config,
|
|
||||||
required ClashConfig clashConfig,
|
|
||||||
bool focus = false,
|
|
||||||
}) async {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Platform.isLinux) {
|
|
||||||
await _updateSystemTray(
|
|
||||||
brightness: appState.brightness,
|
|
||||||
force: focus,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
List<MenuItem> menuItems = [];
|
|
||||||
final showMenuItem = MenuItem(
|
|
||||||
label: appLocalizations.show,
|
|
||||||
onClick: (_) {
|
|
||||||
window?.show();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
menuItems.add(showMenuItem);
|
|
||||||
final startMenuItem = MenuItem.checkbox(
|
|
||||||
label: appFlowingState.isStart
|
|
||||||
? appLocalizations.stop
|
|
||||||
: appLocalizations.start,
|
|
||||||
onClick: (_) async {
|
|
||||||
globalState.appController.updateStart();
|
|
||||||
},
|
|
||||||
checked: false,
|
|
||||||
);
|
|
||||||
menuItems.add(startMenuItem);
|
|
||||||
menuItems.add(MenuItem.separator());
|
|
||||||
for (final mode in Mode.values) {
|
|
||||||
menuItems.add(
|
|
||||||
MenuItem.checkbox(
|
|
||||||
label: Intl.message(mode.name),
|
|
||||||
onClick: (_) {
|
|
||||||
globalState.appController.changeMode(mode);
|
|
||||||
},
|
|
||||||
checked: mode == clashConfig.mode,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
menuItems.add(MenuItem.separator());
|
|
||||||
if (!Platform.isWindows) {
|
|
||||||
final groups = appState.currentGroups;
|
|
||||||
for (final group in groups) {
|
|
||||||
List<MenuItem> subMenuItems = [];
|
|
||||||
for (final proxy in group.all) {
|
|
||||||
subMenuItems.add(
|
|
||||||
MenuItem.checkbox(
|
|
||||||
label: proxy.name,
|
|
||||||
checked: appState.selectedMap[group.name] == proxy.name,
|
|
||||||
onClick: (_) {
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.config.updateCurrentSelectedMap(
|
|
||||||
group.name,
|
|
||||||
proxy.name,
|
|
||||||
);
|
|
||||||
appController.changeProxy(
|
|
||||||
groupName: group.name,
|
|
||||||
proxyName: proxy.name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
menuItems.add(
|
|
||||||
MenuItem.submenu(
|
|
||||||
label: group.name,
|
|
||||||
submenu: Menu(
|
|
||||||
items: subMenuItems,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (groups.isNotEmpty) {
|
|
||||||
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.networkProps.systemProxy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
menuItems.add(MenuItem.separator());
|
|
||||||
}
|
|
||||||
final autoStartMenuItem = MenuItem.checkbox(
|
|
||||||
label: appLocalizations.autoLaunch,
|
|
||||||
onClick: (_) async {
|
|
||||||
globalState.appController.updateAutoLaunch();
|
|
||||||
},
|
|
||||||
checked: config.appSetting.autoLaunch,
|
|
||||||
);
|
|
||||||
final copyEnvVarMenuItem = MenuItem(
|
|
||||||
label: appLocalizations.copyEnvVar,
|
|
||||||
onClick: (_) async {
|
|
||||||
await _copyEnv(clashConfig.mixedPort);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
menuItems.add(autoStartMenuItem);
|
|
||||||
menuItems.add(copyEnvVarMenuItem);
|
|
||||||
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(
|
|
||||||
brightness: appState.brightness,
|
|
||||||
force: focus,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _copyEnv(int port) async {
|
|
||||||
final url = "http://127.0.0.1:$port";
|
|
||||||
|
|
||||||
final cmdline = Platform.isWindows
|
|
||||||
? "set \$env:all_proxy=$url"
|
|
||||||
: "export all_proxy=$url";
|
|
||||||
|
|
||||||
await Clipboard.setData(
|
|
||||||
ClipboardData(
|
|
||||||
text: cmdline,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final tray = Tray();
|
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/config.dart';
|
import 'package:fl_clash/models/config.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:screen_retriever/screen_retriever.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:windows_single_instance/windows_single_instance.dart';
|
||||||
|
|
||||||
|
import 'protocol.dart';
|
||||||
|
import 'system.dart';
|
||||||
|
|
||||||
class Window {
|
class Window {
|
||||||
init(WindowProps props, int version) async {
|
init(WindowProps props, int version) async {
|
||||||
final acquire = await singleInstanceLock.acquire();
|
|
||||||
if (!acquire) {
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
|
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
|
||||||
protocol.register("clash");
|
protocol.register("clash");
|
||||||
protocol.register("clashmeta");
|
protocol.register("clashmeta");
|
||||||
protocol.register("flclash");
|
protocol.register("flclash");
|
||||||
@@ -22,37 +21,15 @@ class Window {
|
|||||||
size: Size(props.width, props.height),
|
size: Size(props.width, props.height),
|
||||||
minimumSize: const Size(380, 500),
|
minimumSize: const Size(380, 500),
|
||||||
);
|
);
|
||||||
if (!Platform.isMacOS || version > 10) {
|
if (props.left != null || props.top != null) {
|
||||||
|
await windowManager.setPosition(
|
||||||
|
Offset(props.left ?? 0, props.top ?? 0),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await windowManager.setAlignment(Alignment.center);
|
||||||
|
}
|
||||||
|
if(!Platform.isMacOS || version > 10){
|
||||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||||
final left = props.left ?? 0;
|
|
||||||
final top = props.top ?? 0;
|
|
||||||
final right = left + props.width;
|
|
||||||
final bottom = top + props.height;
|
|
||||||
if (left == 0 && top == 0) {
|
|
||||||
await windowManager.setAlignment(Alignment.center);
|
|
||||||
} else {
|
|
||||||
final displays = await screenRetriever.getAllDisplays();
|
|
||||||
final isPositionValid = displays.any(
|
|
||||||
(display) {
|
|
||||||
final displayBounds = Rect.fromLTWH(
|
|
||||||
display.visiblePosition!.dx,
|
|
||||||
display.visiblePosition!.dy,
|
|
||||||
display.size.width,
|
|
||||||
display.size.height,
|
|
||||||
);
|
|
||||||
return displayBounds.contains(Offset(left, top)) ||
|
|
||||||
displayBounds.contains(Offset(right, bottom));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (isPositionValid) {
|
|
||||||
await windowManager.setPosition(
|
|
||||||
Offset(
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
await windowManager.setPreventClose(true);
|
await windowManager.setPreventClose(true);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
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:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class Windows {
|
class Windows {
|
||||||
@@ -54,84 +51,12 @@ class Windows {
|
|||||||
calloc.free(argumentsPtr);
|
calloc.free(argumentsPtr);
|
||||||
calloc.free(operationPtr);
|
calloc.free(operationPtr);
|
||||||
|
|
||||||
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
|
if (result <= 32) {
|
||||||
|
|
||||||
if (result < 42) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_killProcess(int port) async {
|
|
||||||
final result = await Process.run('netstat', ['-ano']);
|
|
||||||
final lines = result.stdout.toString().trim().split('\n');
|
|
||||||
for (final line in lines) {
|
|
||||||
if (!line.contains(":$port") || !line.contains("LISTENING")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final parts = line.trim().split(RegExp(r'\s+'));
|
|
||||||
final pid = int.tryParse(parts.last);
|
|
||||||
if (pid != null) {
|
|
||||||
await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<WindowsHelperServiceStatus> checkService() async {
|
|
||||||
// final qcResult = await Process.run('sc', ['qc', appHelperService]);
|
|
||||||
// final qcOutput = qcResult.stdout.toString();
|
|
||||||
// if (qcResult.exitCode != 0 || !qcOutput.contains(appPath.helperPath)) {
|
|
||||||
// return WindowsHelperServiceStatus.none;
|
|
||||||
// }
|
|
||||||
final result = await Process.run('sc', ['query', appHelperService]);
|
|
||||||
if(result.exitCode != 0){
|
|
||||||
return WindowsHelperServiceStatus.none;
|
|
||||||
}
|
|
||||||
final output = result.stdout.toString();
|
|
||||||
if (output.contains("RUNNING") && await request.pingHelper()) {
|
|
||||||
return WindowsHelperServiceStatus.running;
|
|
||||||
}
|
|
||||||
return WindowsHelperServiceStatus.presence;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> registerService() async {
|
|
||||||
final status = await checkService();
|
|
||||||
|
|
||||||
if (status == WindowsHelperServiceStatus.running) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _killProcess(helperPort);
|
|
||||||
|
|
||||||
final command = [
|
|
||||||
"/c",
|
|
||||||
if (status == WindowsHelperServiceStatus.presence) ...[
|
|
||||||
"sc",
|
|
||||||
"delete",
|
|
||||||
appHelperService,
|
|
||||||
"/force",
|
|
||||||
"&&",
|
|
||||||
],
|
|
||||||
"sc",
|
|
||||||
"create",
|
|
||||||
appHelperService,
|
|
||||||
'binPath= "${appPath.helperPath}"',
|
|
||||||
'start= auto',
|
|
||||||
"&&",
|
|
||||||
"sc",
|
|
||||||
"start",
|
|
||||||
appHelperService,
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
final res = runas("cmd.exe", command);
|
|
||||||
|
|
||||||
await Future.delayed(
|
|
||||||
Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> registerTask(String appName) async {
|
Future<bool> registerTask(String appName) async {
|
||||||
final taskXml = '''
|
final taskXml = '''
|
||||||
<?xml version="1.0" encoding="UTF-16"?>
|
<?xml version="1.0" encoding="UTF-16"?>
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ import 'dart:isolate';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:fl_clash/clash/clash.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 'common/common.dart';
|
import 'clash/core.dart';
|
||||||
import 'models/models.dart';
|
import 'models/models.dart';
|
||||||
|
import 'common/common.dart';
|
||||||
|
|
||||||
class AppController {
|
class AppController {
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
@@ -23,81 +25,48 @@ class AppController {
|
|||||||
late AppFlowingState appFlowingState;
|
late AppFlowingState appFlowingState;
|
||||||
late Config config;
|
late Config config;
|
||||||
late ClashConfig clashConfig;
|
late ClashConfig clashConfig;
|
||||||
|
late Function updateClashConfigDebounce;
|
||||||
|
late Function updateGroupDebounce;
|
||||||
|
late Function addCheckIpNumDebounce;
|
||||||
|
late Function applyProfileDebounce;
|
||||||
|
|
||||||
AppController(this.context) {
|
AppController(this.context) {
|
||||||
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>();
|
appFlowingState = context.read<AppFlowingState>();
|
||||||
}
|
updateClashConfigDebounce = debounce<Function()>(() async {
|
||||||
|
await updateClashConfig();
|
||||||
updateClashConfigDebounce() {
|
});
|
||||||
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
applyProfileDebounce = debounce<Function()>(() async {
|
||||||
}
|
await applyProfile(isPrue: true);
|
||||||
|
});
|
||||||
updateGroupsDebounce() {
|
addCheckIpNumDebounce = debounce(() {
|
||||||
debouncer.call(DebounceTag.updateGroups, updateGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
addCheckIpNumDebounce() {
|
|
||||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
|
||||||
appState.checkIpNum++;
|
appState.checkIpNum++;
|
||||||
});
|
});
|
||||||
}
|
updateGroupDebounce = debounce(() async {
|
||||||
|
|
||||||
applyProfileDebounce() {
|
|
||||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
|
||||||
applyProfile(isPrue: true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
savePreferencesDebounce() {
|
|
||||||
debouncer.call(DebounceTag.savePreferences, savePreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeProxyDebounce(String groupName, String proxyName) {
|
|
||||||
debouncer.call(DebounceTag.changeProxy,
|
|
||||||
(String groupName, String proxyName) async {
|
|
||||||
await changeProxy(
|
|
||||||
groupName: groupName,
|
|
||||||
proxyName: proxyName,
|
|
||||||
);
|
|
||||||
await updateGroups();
|
await updateGroups();
|
||||||
}, args: [groupName, proxyName]);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
restartCore() async {
|
|
||||||
await globalState.restartCore(
|
|
||||||
appState: appState,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(bool isStart) async {
|
updateStatus(bool isStart) async {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
await globalState.handleStart();
|
await globalState.handleStart(
|
||||||
|
config: config,
|
||||||
|
clashConfig: clashConfig,
|
||||||
|
);
|
||||||
updateRunTime();
|
updateRunTime();
|
||||||
updateTraffic();
|
updateTraffic();
|
||||||
globalState.updateFunctionLists = [
|
globalState.updateFunctionLists = [
|
||||||
updateRunTime,
|
updateRunTime,
|
||||||
updateTraffic,
|
updateTraffic,
|
||||||
];
|
];
|
||||||
final currentLastModified =
|
if (!Platform.isAndroid) {
|
||||||
await config.getCurrentProfile()?.profileLastModified;
|
applyProfileDebounce();
|
||||||
if (currentLastModified == null ||
|
|
||||||
globalState.lastProfileModified == null) {
|
|
||||||
addCheckIpNumDebounce();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
|
|
||||||
addCheckIpNumDebounce();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyProfileDebounce();
|
|
||||||
} else {
|
} else {
|
||||||
await globalState.handleStop();
|
await globalState.handleStop();
|
||||||
await clashCore.resetTraffic();
|
clashCore.resetTraffic();
|
||||||
appFlowingState.traffics = [];
|
appFlowingState.traffics = [];
|
||||||
appFlowingState.totalTraffic = Traffic();
|
appFlowingState.totalTraffic = Traffic();
|
||||||
appFlowingState.runTime = null;
|
appFlowingState.runTime = null;
|
||||||
@@ -105,6 +74,10 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCoreVersionInfo() {
|
||||||
|
globalState.updateCoreVersionInfo(appState);
|
||||||
|
}
|
||||||
|
|
||||||
updateRunTime() {
|
updateRunTime() {
|
||||||
final startTime = globalState.startTime;
|
final startTime = globalState.startTime;
|
||||||
if (startTime != null) {
|
if (startTime != null) {
|
||||||
@@ -118,7 +91,6 @@ class AppController {
|
|||||||
|
|
||||||
updateTraffic() {
|
updateTraffic() {
|
||||||
globalState.updateTraffic(
|
globalState.updateTraffic(
|
||||||
config: config,
|
|
||||||
appFlowingState: appFlowingState,
|
appFlowingState: appFlowingState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -131,7 +103,7 @@ class AppController {
|
|||||||
|
|
||||||
deleteProfile(String id) async {
|
deleteProfile(String id) async {
|
||||||
config.deleteProfileById(id);
|
config.deleteProfileById(id);
|
||||||
clearEffect(id);
|
clashCore.clearEffect(id);
|
||||||
if (config.currentProfileId == id) {
|
if (config.currentProfileId == id) {
|
||||||
if (config.profiles.isNotEmpty) {
|
if (config.profiles.isNotEmpty) {
|
||||||
final updateId = config.profiles.first.id;
|
final updateId = config.profiles.first.id;
|
||||||
@@ -143,24 +115,11 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProviders() async {
|
|
||||||
await globalState.updateProviders(appState);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLocalIp() async {
|
|
||||||
appFlowingState.localIp = null;
|
|
||||||
await Future.delayed(commonDuration);
|
|
||||||
appFlowingState.localIp = await other.getLocalIpAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateProfile(Profile profile) async {
|
Future<void> updateProfile(Profile profile) async {
|
||||||
final newProfile = await profile.update();
|
final newProfile = await profile.update();
|
||||||
config.setProfile(
|
config.setProfile(
|
||||||
newProfile.copyWith(isUpdating: false),
|
newProfile.copyWith(isUpdating: false),
|
||||||
);
|
);
|
||||||
if (profile.id == config.currentProfile?.id) {
|
|
||||||
applyProfileDebounce();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
Future<void> updateClashConfig({bool isPatch = true}) async {
|
||||||
@@ -168,7 +127,6 @@ class AppController {
|
|||||||
if (commonScaffoldState?.mounted != true) return;
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
await commonScaffoldState?.loadingRun(() async {
|
await commonScaffoldState?.loadingRun(() async {
|
||||||
await globalState.updateClashConfig(
|
await globalState.updateClashConfig(
|
||||||
appState: appState,
|
|
||||||
clashConfig: clashConfig,
|
clashConfig: clashConfig,
|
||||||
config: config,
|
config: config,
|
||||||
isPatch: isPatch,
|
isPatch: isPatch,
|
||||||
@@ -244,16 +202,25 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
savePreferences() async {
|
savePreferences() async {
|
||||||
debugPrint("[APP] savePreferences");
|
await saveConfigPreferences();
|
||||||
|
await saveClashConfigPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfigPreferences() async {
|
||||||
|
debugPrint("saveConfigPreferences");
|
||||||
await preferences.saveConfig(config);
|
await preferences.saveConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveClashConfigPreferences() async {
|
||||||
|
debugPrint("saveClashConfigPreferences");
|
||||||
await preferences.saveClashConfig(clashConfig);
|
await preferences.saveClashConfig(clashConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeProxy({
|
changeProxy({
|
||||||
required String groupName,
|
required String groupName,
|
||||||
required String proxyName,
|
required String proxyName,
|
||||||
}) async {
|
}) {
|
||||||
await globalState.changeProxy(
|
globalState.changeProxy(
|
||||||
config: config,
|
config: config,
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
proxyName: proxyName,
|
proxyName: proxyName,
|
||||||
@@ -264,7 +231,7 @@ class AppController {
|
|||||||
handleBackOrExit() async {
|
handleBackOrExit() async {
|
||||||
if (config.appSetting.minimizeOnExit) {
|
if (config.appSetting.minimizeOnExit) {
|
||||||
if (system.isDesktop) {
|
if (system.isDesktop) {
|
||||||
await savePreferencesDebounce();
|
await savePreferences();
|
||||||
}
|
}
|
||||||
await system.back();
|
await system.back();
|
||||||
} else {
|
} else {
|
||||||
@@ -273,16 +240,22 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleExit() async {
|
handleExit() async {
|
||||||
try {
|
await updateStatus(false);
|
||||||
await updateStatus(false);
|
await proxy?.stopProxy();
|
||||||
await clashCore.shutdown();
|
await savePreferences();
|
||||||
await clashService?.destroy();
|
clashCore.shutdown();
|
||||||
await proxy?.stopProxy();
|
|
||||||
await savePreferences();
|
|
||||||
} catch (_) {}
|
|
||||||
system.exit();
|
system.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateLogStatus() {
|
||||||
|
if (config.appSetting.openLogs) {
|
||||||
|
clashCore.startLog();
|
||||||
|
} else {
|
||||||
|
clashCore.stopLog();
|
||||||
|
appFlowingState.logs = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
autoCheckUpdate() async {
|
autoCheckUpdate() async {
|
||||||
if (!config.appSetting.autoCheckUpdate) return;
|
if (!config.appSetting.autoCheckUpdate) return;
|
||||||
final res = await request.checkForUpdate();
|
final res = await request.checkForUpdate();
|
||||||
@@ -337,33 +310,20 @@ class AppController {
|
|||||||
if (!isDisclaimerAccepted) {
|
if (!isDisclaimerAccepted) {
|
||||||
handleExit();
|
handleExit();
|
||||||
}
|
}
|
||||||
|
updateLogStatus();
|
||||||
if (!config.appSetting.silentLaunch) {
|
if (!config.appSetting.silentLaunch) {
|
||||||
window?.show();
|
window?.show();
|
||||||
}
|
}
|
||||||
await globalState.initCore(
|
|
||||||
appState: appState,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
|
||||||
);
|
|
||||||
await _initStatus();
|
|
||||||
autoLaunch?.updateStatus(
|
|
||||||
config.appSetting.autoLaunch,
|
|
||||||
);
|
|
||||||
autoUpdateProfiles();
|
|
||||||
autoCheckUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
_initStatus() async {
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
globalState.updateStartTime();
|
globalState.updateStartTime();
|
||||||
}
|
}
|
||||||
final status =
|
if (globalState.isStart) {
|
||||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
await updateStatus(true);
|
||||||
|
} else {
|
||||||
await updateStatus(status);
|
await updateStatus(config.appSetting.autoRun);
|
||||||
if (!status) {
|
|
||||||
addCheckIpNumDebounce();
|
|
||||||
}
|
}
|
||||||
|
autoUpdateProfiles();
|
||||||
|
autoCheckUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
setDelay(Delay delay) {
|
setDelay(Delay delay) {
|
||||||
@@ -424,6 +384,10 @@ class AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSnackBar(String message) {
|
||||||
|
globalState.showSnackBar(context, message: message);
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> showDisclaimer() async {
|
Future<bool> showDisclaimer() async {
|
||||||
return await globalState.showCommonDialog<bool>(
|
return await globalState.showCommonDialog<bool>(
|
||||||
dismissible: false,
|
dismissible: false,
|
||||||
@@ -567,19 +531,6 @@ class AppController {
|
|||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEffect(String profileId) async {
|
|
||||||
final profilePath = await appPath.getProfilePath(profileId);
|
|
||||||
final providersPath = await appPath.getProvidersPath(profileId);
|
|
||||||
return await Isolate.run(() async {
|
|
||||||
if (profilePath != null) {
|
|
||||||
await File(profilePath).delete(recursive: true);
|
|
||||||
}
|
|
||||||
if (providersPath != null) {
|
|
||||||
await File(providersPath).delete(recursive: true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTun() {
|
updateTun() {
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
clashConfig.tun = clashConfig.tun.copyWith(
|
||||||
enable: !clashConfig.tun.enable,
|
enable: !clashConfig.tun.enable,
|
||||||
@@ -587,8 +538,8 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSystemProxy() {
|
updateSystemProxy() {
|
||||||
config.networkProps = config.networkProps.copyWith(
|
config.desktopProps = config.desktopProps.copyWith(
|
||||||
systemProxy: !config.networkProps.systemProxy,
|
systemProxy: !config.desktopProps.systemProxy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,20 +547,18 @@ class AppController {
|
|||||||
updateStatus(!appFlowingState.isStart);
|
updateStatus(!appFlowingState.isStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMode(Mode mode) {
|
|
||||||
clashConfig.mode = mode;
|
|
||||||
if (mode == Mode.global) {
|
|
||||||
config.updateCurrentGroupName(GroupName.GLOBAL.name);
|
|
||||||
}
|
|
||||||
addCheckIpNumDebounce();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAutoLaunch() {
|
updateAutoLaunch() {
|
||||||
config.appSetting = config.appSetting.copyWith(
|
config.appSetting = config.appSetting.copyWith(
|
||||||
autoLaunch: !config.appSetting.autoLaunch,
|
autoLaunch: !config.appSetting.autoLaunch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAdminAutoLaunch() {
|
||||||
|
config.appSetting = config.appSetting.copyWith(
|
||||||
|
adminAutoLaunch: !config.appSetting.adminAutoLaunch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateVisible() async {
|
updateVisible() async {
|
||||||
final visible = await window?.isVisible();
|
final visible = await window?.isVisible();
|
||||||
if (visible != null && !visible) {
|
if (visible != null && !visible) {
|
||||||
@@ -658,14 +607,122 @@ class AppController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTray([bool focus = false]) async {
|
Future _updateSystemTray({
|
||||||
tray.update(
|
required bool isStart,
|
||||||
appState: appState,
|
required Brightness? brightness,
|
||||||
appFlowingState: appFlowingState,
|
bool force = false,
|
||||||
config: config,
|
}) async {
|
||||||
clashConfig: clashConfig,
|
if (Platform.isLinux || force) {
|
||||||
focus: focus,
|
await trayManager.destroy();
|
||||||
|
}
|
||||||
|
await trayManager.setIcon(
|
||||||
|
other.getTrayIconPath(
|
||||||
|
isStart: isStart,
|
||||||
|
brightness: brightness ??
|
||||||
|
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||||
|
),
|
||||||
|
isTemplate: true,
|
||||||
);
|
);
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
await trayManager.setToolTip(
|
||||||
|
appName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTray([bool focus = false]) async {
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
await _updateSystemTray(
|
||||||
|
isStart: appFlowingState.isStart,
|
||||||
|
brightness: appState.brightness,
|
||||||
|
force: focus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<MenuItem> menuItems = [];
|
||||||
|
final showMenuItem = MenuItem(
|
||||||
|
label: appLocalizations.show,
|
||||||
|
onClick: (_) {
|
||||||
|
window?.show();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
menuItems.add(showMenuItem);
|
||||||
|
final startMenuItem = MenuItem.checkbox(
|
||||||
|
label: appFlowingState.isStart
|
||||||
|
? appLocalizations.stop
|
||||||
|
: appLocalizations.start,
|
||||||
|
onClick: (_) async {
|
||||||
|
globalState.appController.updateStart();
|
||||||
|
},
|
||||||
|
checked: false,
|
||||||
|
);
|
||||||
|
menuItems.add(startMenuItem);
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
for (final mode in Mode.values) {
|
||||||
|
menuItems.add(
|
||||||
|
MenuItem.checkbox(
|
||||||
|
label: Intl.message(mode.name),
|
||||||
|
onClick: (_) {
|
||||||
|
globalState.appController.clashConfig.mode = mode;
|
||||||
|
},
|
||||||
|
checked: mode == clashConfig.mode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
if (appFlowingState.isStart) {
|
||||||
|
menuItems.add(
|
||||||
|
MenuItem.checkbox(
|
||||||
|
label: appLocalizations.tun,
|
||||||
|
onClick: (_) {
|
||||||
|
globalState.appController.updateTun();
|
||||||
|
},
|
||||||
|
checked: clashConfig.tun.enable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
menuItems.add(
|
||||||
|
MenuItem.checkbox(
|
||||||
|
label: appLocalizations.systemProxy,
|
||||||
|
onClick: (_) {
|
||||||
|
globalState.appController.updateSystemProxy();
|
||||||
|
},
|
||||||
|
checked: config.desktopProps.systemProxy,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
}
|
||||||
|
final autoStartMenuItem = MenuItem.checkbox(
|
||||||
|
label: appLocalizations.autoLaunch,
|
||||||
|
onClick: (_) async {
|
||||||
|
globalState.appController.updateAutoLaunch();
|
||||||
|
},
|
||||||
|
checked: config.appSetting.autoLaunch,
|
||||||
|
);
|
||||||
|
final adminAutoStartMenuItem = MenuItem.checkbox(
|
||||||
|
label: appLocalizations.adminAutoLaunch,
|
||||||
|
onClick: (_) async {
|
||||||
|
globalState.appController.updateAdminAutoLaunch();
|
||||||
|
},
|
||||||
|
checked: config.appSetting.adminAutoLaunch,
|
||||||
|
);
|
||||||
|
menuItems.add(autoStartMenuItem);
|
||||||
|
menuItems.add(adminAutoStartMenuItem);
|
||||||
|
menuItems.add(MenuItem.separator());
|
||||||
|
final exitMenuItem = MenuItem(
|
||||||
|
label: appLocalizations.exit,
|
||||||
|
onClick: (_) async {
|
||||||
|
await globalState.appController.handleExit();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
menuItems.add(exitMenuItem);
|
||||||
|
final menu = Menu(items: menuItems);
|
||||||
|
await trayManager.setContextMenu(menu);
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
await _updateSystemTray(
|
||||||
|
isStart: appFlowingState.isStart,
|
||||||
|
brightness: appState.brightness,
|
||||||
|
force: focus,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recoveryData(
|
recoveryData(
|
||||||
|
|||||||
@@ -1,39 +1,9 @@
|
|||||||
// ignore_for_file: constant_identifier_names
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_clash/fragments/dashboard/widgets/widgets.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
|
|
||||||
enum SupportPlatform {
|
|
||||||
Windows,
|
|
||||||
MacOS,
|
|
||||||
Linux,
|
|
||||||
Android;
|
|
||||||
|
|
||||||
static SupportPlatform get currentPlatform {
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
return SupportPlatform.Windows;
|
|
||||||
} else if (Platform.isMacOS) {
|
|
||||||
return SupportPlatform.MacOS;
|
|
||||||
} else if (Platform.isLinux) {
|
|
||||||
return SupportPlatform.Linux;
|
|
||||||
} else if (Platform.isAndroid) {
|
|
||||||
return SupportPlatform.Android;
|
|
||||||
}
|
|
||||||
throw "invalid platform";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const desktopPlatforms = [
|
|
||||||
SupportPlatform.Linux,
|
|
||||||
SupportPlatform.MacOS,
|
|
||||||
SupportPlatform.Windows,
|
|
||||||
];
|
|
||||||
|
|
||||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
||||||
|
|
||||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||||
@@ -121,10 +91,6 @@ enum RecoveryOption {
|
|||||||
enum ChipType { action, delete }
|
enum ChipType { action, delete }
|
||||||
|
|
||||||
enum CommonCardType { plain, filled }
|
enum CommonCardType { plain, filled }
|
||||||
//
|
|
||||||
// extension CommonCardTypeExt on CommonCardType {
|
|
||||||
// CommonCardType get variant => CommonCardType.plain;
|
|
||||||
// }
|
|
||||||
|
|
||||||
enum ProxiesType { tab, list }
|
enum ProxiesType { tab, list }
|
||||||
|
|
||||||
@@ -196,140 +162,3 @@ enum ProxiesIconStyle {
|
|||||||
none,
|
none,
|
||||||
icon,
|
icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FontFamily {
|
|
||||||
system(),
|
|
||||||
miSans("MiSans"),
|
|
||||||
twEmoji("Twemoji"),
|
|
||||||
icon("Icons");
|
|
||||||
|
|
||||||
final String? value;
|
|
||||||
|
|
||||||
const FontFamily([this.value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RouteMode {
|
|
||||||
bypassPrivate,
|
|
||||||
config,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ActionMethod {
|
|
||||||
message,
|
|
||||||
initClash,
|
|
||||||
getIsInit,
|
|
||||||
forceGc,
|
|
||||||
shutdown,
|
|
||||||
validateConfig,
|
|
||||||
updateConfig,
|
|
||||||
getProxies,
|
|
||||||
changeProxy,
|
|
||||||
getTraffic,
|
|
||||||
getTotalTraffic,
|
|
||||||
resetTraffic,
|
|
||||||
asyncTestDelay,
|
|
||||||
getConnections,
|
|
||||||
closeConnections,
|
|
||||||
closeConnection,
|
|
||||||
getExternalProviders,
|
|
||||||
getExternalProvider,
|
|
||||||
updateGeoData,
|
|
||||||
updateExternalProvider,
|
|
||||||
sideLoadExternalProvider,
|
|
||||||
startLog,
|
|
||||||
stopLog,
|
|
||||||
startListener,
|
|
||||||
stopListener,
|
|
||||||
getCountryCode,
|
|
||||||
getMemory,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AuthorizeCode { none, success, error }
|
|
||||||
|
|
||||||
enum WindowsHelperServiceStatus {
|
|
||||||
none,
|
|
||||||
presence,
|
|
||||||
running,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DebounceTag {
|
|
||||||
updateClashConfig,
|
|
||||||
updateGroups,
|
|
||||||
addCheckIpNum,
|
|
||||||
applyProfile,
|
|
||||||
savePreferences,
|
|
||||||
changeProxy,
|
|
||||||
checkIp,
|
|
||||||
handleWill,
|
|
||||||
updateDelay,
|
|
||||||
vpnTip,
|
|
||||||
autoLaunch
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DashboardWidget {
|
|
||||||
networkSpeed(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 8,
|
|
||||||
child: NetworkSpeed(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
outboundMode(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: OutboundMode(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trafficUsage(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: TrafficUsage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
networkDetection(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: NetworkDetection(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
tunButton(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: TUNButton(),
|
|
||||||
),
|
|
||||||
platforms: desktopPlatforms,
|
|
||||||
),
|
|
||||||
systemProxyButton(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: SystemProxyButton(),
|
|
||||||
),
|
|
||||||
platforms: desktopPlatforms,
|
|
||||||
),
|
|
||||||
intranetIp(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: IntranetIP(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
memoryInfo(
|
|
||||||
GridItem(
|
|
||||||
crossAxisCellCount: 4,
|
|
||||||
child: MemoryInfo(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final GridItem widget;
|
|
||||||
final List<SupportPlatform> platforms;
|
|
||||||
|
|
||||||
const DashboardWidget(
|
|
||||||
this.widget, {
|
|
||||||
this.platforms = SupportPlatform.values,
|
|
||||||
});
|
|
||||||
|
|
||||||
static DashboardWidget getDashboardWidget(GridItem gridItem) {
|
|
||||||
final dashboardWidgets = DashboardWidget.values;
|
|
||||||
final index = dashboardWidgets.indexWhere(
|
|
||||||
(item) => item.widget == gridItem,
|
|
||||||
);
|
|
||||||
return dashboardWidgets[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -104,9 +104,11 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
showSheet(
|
showSheet(
|
||||||
title: appLocalizations.proxiesSetting,
|
title: appLocalizations.proxiesSetting,
|
||||||
context: context,
|
context: context,
|
||||||
body: AccessControlWidget(
|
builder: (_) {
|
||||||
context: context,
|
return AccessControlWidget(
|
||||||
),
|
context: context,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.tune),
|
icon: const Icon(Icons.tune),
|
||||||
@@ -176,8 +178,8 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
status: !isAccessControl,
|
status: !isAccessControl,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ActivateBox(
|
AbsorbPointer(
|
||||||
active: isAccessControl,
|
absorbing: !isAccessControl,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 4,
|
top: 4,
|
||||||
@@ -330,8 +332,8 @@ class PackageListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActivateBox(
|
return AbsorbPointer(
|
||||||
active: isActive,
|
absorbing: !isActive,
|
||||||
child: ListItem.checkbox(
|
child: ListItem.checkbox(
|
||||||
leading: SizedBox(
|
leading: SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
|
|||||||
@@ -60,6 +60,32 @@ class UsageSwitch extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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});
|
||||||
|
|
||||||
@@ -108,6 +134,8 @@ class ApplicationSettingFragment extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if(Platform.isWindows)
|
||||||
|
const AdminAutoLaunchItem(),
|
||||||
if (system.isDesktop)
|
if (system.isDesktop)
|
||||||
Selector<Config, bool>(
|
Selector<Config, bool>(
|
||||||
selector: (_, config) => config.appSetting.silentLaunch,
|
selector: (_, config) => config.appSetting.silentLaunch,
|
||||||
|
|||||||
@@ -343,8 +343,8 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_obscureController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
_obscureController.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
|
|||||||
title: appLocalizations.network,
|
title: appLocalizations.network,
|
||||||
isScaffold: true,
|
isScaffold: true,
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
extendPageWidth: 360,
|
|
||||||
widget: const NetworkListView(),
|
widget: const NetworkListView(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ 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:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class VPNItem extends StatelessWidget {
|
class VPNSwitch extends StatelessWidget {
|
||||||
const VPNItem({super.key});
|
const VPNSwitch({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -40,8 +39,8 @@ class TUNItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<ClashConfig, bool>(
|
return Selector<Config, bool>(
|
||||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
selector: (_, config) => config.vpnProps.enable,
|
||||||
builder: (_, enable, __) {
|
builder: (_, enable, __) {
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.tun),
|
title: Text(appLocalizations.tun),
|
||||||
@@ -61,8 +60,8 @@ class TUNItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllowBypassItem extends StatelessWidget {
|
class AllowBypassSwitch extends StatelessWidget {
|
||||||
const AllowBypassItem({super.key});
|
const AllowBypassSwitch({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -88,8 +87,8 @@ class AllowBypassItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VpnSystemProxyItem extends StatelessWidget {
|
class SystemProxySwitch extends StatelessWidget {
|
||||||
const VpnSystemProxyItem({super.key});
|
const SystemProxySwitch({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -115,35 +114,8 @@ class VpnSystemProxyItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemProxyItem extends StatelessWidget {
|
class Ipv6Switch extends StatelessWidget {
|
||||||
const SystemProxyItem({super.key});
|
const Ipv6Switch({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.networkProps.systemProxy,
|
|
||||||
builder: (_, systemProxy, __) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
title: Text(appLocalizations.systemProxy),
|
|
||||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: systemProxy,
|
|
||||||
onChanged: (bool value) async {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
final networkProps = config.networkProps;
|
|
||||||
config.networkProps = networkProps.copyWith(
|
|
||||||
systemProxy: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Ipv6Item extends StatelessWidget {
|
|
||||||
const Ipv6Item({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -204,36 +176,6 @@ class TunStackItem extends StatelessWidget {
|
|||||||
class BypassDomainItem extends StatelessWidget {
|
class BypassDomainItem extends StatelessWidget {
|
||||||
const BypassDomainItem({super.key});
|
const BypassDomainItem({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
globalState.showMessage(
|
|
||||||
title: appLocalizations.reset,
|
|
||||||
message: TextSpan(
|
|
||||||
text: appLocalizations.resetTip,
|
|
||||||
),
|
|
||||||
onTab: () {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.networkProps = config.networkProps.copyWith(
|
|
||||||
bypassDomain: defaultBypassDomain,
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
tooltip: appLocalizations.reset,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.replay,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListItem.open(
|
return ListItem.open(
|
||||||
@@ -241,20 +183,19 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
subtitle: Text(appLocalizations.bypassDomainDesc),
|
subtitle: Text(appLocalizations.bypassDomainDesc),
|
||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
isScaffold: true,
|
|
||||||
title: appLocalizations.bypassDomain,
|
title: appLocalizations.bypassDomain,
|
||||||
widget: Selector<Config, List<String>>(
|
widget: Selector<Config, List<String>>(
|
||||||
selector: (_, config) => config.networkProps.bypassDomain,
|
selector: (_, config) => config.vpnProps.bypassDomain,
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
shouldRebuild: (prev, next) =>
|
||||||
builder: (context, bypassDomain, __) {
|
!stringListEquality.equals(prev, next),
|
||||||
_initActions(context);
|
builder: (_, bypassDomain, __) {
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.bypassDomain,
|
title: appLocalizations.bypassDomain,
|
||||||
items: bypassDomain,
|
items: bypassDomain,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items){
|
||||||
final config = globalState.appController.config;
|
final config = globalState.appController.config;
|
||||||
config.networkProps = config.networkProps.copyWith(
|
config.vpnProps = config.vpnProps.copyWith(
|
||||||
bypassDomain: List.from(items),
|
bypassDomain: List.from(items),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -267,108 +208,22 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteModeItem extends StatelessWidget {
|
|
||||||
const RouteModeItem({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<ClashConfig, RouteMode>(
|
|
||||||
selector: (_, clashConfig) => clashConfig.routeMode,
|
|
||||||
builder: (_, value, __) {
|
|
||||||
return ListItem<RouteMode>.options(
|
|
||||||
title: Text(appLocalizations.routeMode),
|
|
||||||
subtitle: Text(Intl.message("routeMode_${value.name}")),
|
|
||||||
delegate: OptionsDelegate<RouteMode>(
|
|
||||||
title: appLocalizations.routeMode,
|
|
||||||
options: RouteMode.values,
|
|
||||||
onChanged: (RouteMode? value) {
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.clashConfig.routeMode = value;
|
|
||||||
},
|
|
||||||
textBuilder: (routeMode) => Intl.message(
|
|
||||||
"routeMode_${routeMode.name}",
|
|
||||||
),
|
|
||||||
value: value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RouteAddressItem extends StatelessWidget {
|
|
||||||
const RouteAddressItem({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<ClashConfig, bool>(
|
|
||||||
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
|
|
||||||
builder: (_, value, child) {
|
|
||||||
if (value) {
|
|
||||||
return child!;
|
|
||||||
}
|
|
||||||
return Container();
|
|
||||||
},
|
|
||||||
child: ListItem.open(
|
|
||||||
title: Text(appLocalizations.routeAddress),
|
|
||||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
isBlur: false,
|
|
||||||
isScaffold: true,
|
|
||||||
title: appLocalizations.routeAddress,
|
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
|
||||||
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
|
|
||||||
shouldRebuild: (prev, next) =>
|
|
||||||
!stringListEquality.equals(prev, next),
|
|
||||||
builder: (context, routeAddress, __) {
|
|
||||||
return ListPage(
|
|
||||||
title: appLocalizations.routeAddress,
|
|
||||||
items: routeAddress,
|
|
||||||
titleBuilder: (item) => Text(item),
|
|
||||||
onChange: (items) {
|
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
|
||||||
clashConfig.includeRouteAddress =
|
|
||||||
Set<String>.from(items).toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final networkItems = [
|
final networkItems = [
|
||||||
if (Platform.isAndroid) const VPNItem(),
|
Platform.isAndroid ? const VPNSwitch() : const TUNItem(),
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
...generateSection(
|
...generateSection(
|
||||||
title: "VPN",
|
title: "VPN",
|
||||||
items: [
|
items: [
|
||||||
const SystemProxyItem(),
|
const SystemProxySwitch(),
|
||||||
const AllowBypassItem(),
|
const AllowBypassSwitch(),
|
||||||
const Ipv6Item(),
|
const Ipv6Switch(),
|
||||||
],
|
const BypassDomainItem(),
|
||||||
),
|
|
||||||
if (system.isDesktop)
|
|
||||||
...generateSection(
|
|
||||||
title: appLocalizations.system,
|
|
||||||
items: [
|
|
||||||
SystemProxyItem(),
|
|
||||||
BypassDomainItem(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
...generateSection(
|
...generateSection(
|
||||||
title: appLocalizations.options,
|
title: appLocalizations.options,
|
||||||
items: [
|
items: [
|
||||||
if (system.isDesktop) const TUNItem(),
|
|
||||||
const TunStackItem(),
|
const TunStackItem(),
|
||||||
const RouteModeItem(),
|
|
||||||
const RouteAddressItem(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -27,22 +27,18 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
connectionsNotifier.value = connectionsNotifier.value
|
||||||
connections: await clashCore.getConnections(),
|
.copyWith(connections: clashCore.getConnections());
|
||||||
);
|
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
timer = Timer.periodic(
|
timer = Timer.periodic(
|
||||||
const Duration(seconds: 1),
|
const Duration(seconds: 1),
|
||||||
(timer) async {
|
(timer) {
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||||
connections: await clashCore.getConnections(),
|
connections: clashCore.getConnections(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -66,11 +62,14 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
clashCore.closeConnections();
|
clashCore.closeConnections();
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||||
connections: await clashCore.getConnections(),
|
connections: clashCore.getConnections(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete_sweep_outlined),
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
@@ -100,20 +99,19 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleBlockConnection(String id) async {
|
_handleBlockConnection(String id) {
|
||||||
clashCore.closeConnection(id);
|
clashCore.closeConnection(id);
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
connectionsNotifier.value = connectionsNotifier.value
|
||||||
connections: await clashCore.getConnections(),
|
.copyWith(connections: clashCore.getConnections());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
connectionsNotifier.dispose();
|
connectionsNotifier.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
timer = null;
|
timer = null;
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -241,10 +239,10 @@ class ConnectionsSearchDelegate extends SearchDelegate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleBlockConnection(String id) async {
|
_handleBlockConnection(String id) {
|
||||||
clashCore.closeConnection(id);
|
clashCore.closeConnection(id);
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
||||||
connections: await clashCore.getConnections(),
|
connections: clashCore.getConnections(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
|
||||||
|
import 'package:fl_clash/fragments/dashboard/status_switch.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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'widgets/start_button.dart';
|
import 'network_detection.dart';
|
||||||
|
import 'outbound_mode.dart';
|
||||||
|
import 'start_button.dart';
|
||||||
|
import 'network_speed.dart';
|
||||||
|
import 'traffic_usage.dart';
|
||||||
|
|
||||||
class DashboardFragment extends StatefulWidget {
|
class DashboardFragment extends StatefulWidget {
|
||||||
const DashboardFragment({super.key});
|
const DashboardFragment({super.key});
|
||||||
@@ -16,120 +21,64 @@ class DashboardFragment extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardFragmentState extends State<DashboardFragment> {
|
class _DashboardFragmentState extends State<DashboardFragment> {
|
||||||
final key = GlobalKey<SuperGridState>();
|
|
||||||
|
|
||||||
_initScaffold(bool isCurrent) {
|
|
||||||
if (!isCurrent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
ValueListenableBuilder(
|
|
||||||
valueListenable: key.currentState!.addedChildrenNotifier,
|
|
||||||
builder: (_, addedChildren, child) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: key.currentState!.isEditNotifier,
|
|
||||||
builder: (_, isEdit, child) {
|
|
||||||
if (!isEdit || addedChildren.isEmpty) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
key.currentState!.showAddModal();
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.add_circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: ValueListenableBuilder(
|
|
||||||
valueListenable: key.currentState!.isEditNotifier,
|
|
||||||
builder: (_, isEdit, ___) {
|
|
||||||
return isEdit
|
|
||||||
? Icon(Icons.save)
|
|
||||||
: Icon(
|
|
||||||
Icons.edit,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
key.currentState!.isEditNotifier.value =
|
|
||||||
!key.currentState!.isEditNotifier.value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActiveBuilder(
|
return FloatLayout(
|
||||||
label: "dashboard",
|
floatingWidget: const FloatWrapper(
|
||||||
builder: (isCurrent, child) {
|
child: StartButton(),
|
||||||
_initScaffold(isCurrent);
|
),
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16).copyWith(
|
padding: const EdgeInsets.all(16).copyWith(
|
||||||
bottom: 88,
|
bottom: 88,
|
||||||
),
|
),
|
||||||
child: Selector2<AppState, Config, DashboardState>(
|
child: Selector<AppState, double>(
|
||||||
selector: (_, appState, config) => DashboardState(
|
selector: (_, appState) => appState.viewWidth,
|
||||||
dashboardWidgets: config.appSetting.dashboardWidgets,
|
builder: (_, viewWidth, ___) {
|
||||||
viewWidth: appState.viewWidth,
|
final columns = max(4 * ((viewWidth / 350).ceil()), 8);
|
||||||
),
|
final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4;
|
||||||
builder: (_, state, ___) {
|
return Grid(
|
||||||
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
|
|
||||||
return SuperGrid(
|
|
||||||
key: key,
|
|
||||||
crossAxisCount: columns,
|
crossAxisCount: columns,
|
||||||
crossAxisSpacing: 16,
|
crossAxisSpacing: 16,
|
||||||
mainAxisSpacing: 16,
|
mainAxisSpacing: 16,
|
||||||
children: [
|
children: [
|
||||||
...state.dashboardWidgets
|
const GridItem(
|
||||||
.where(
|
crossAxisCellCount: 8,
|
||||||
(item) => item.platforms.contains(
|
child: NetworkSpeed(),
|
||||||
SupportPlatform.currentPlatform,
|
),
|
||||||
),
|
// if (Platform.isAndroid)
|
||||||
)
|
// GridItem(
|
||||||
.map(
|
// crossAxisCellCount: switchCount,
|
||||||
(item) => item.widget,
|
// child: const VPNSwitch(),
|
||||||
),
|
// ),
|
||||||
|
if (system.isDesktop) ...[
|
||||||
|
GridItem(
|
||||||
|
crossAxisCellCount: switchCount,
|
||||||
|
child: const TUNSwitch(),
|
||||||
|
),
|
||||||
|
GridItem(
|
||||||
|
crossAxisCellCount: switchCount,
|
||||||
|
child: const ProxySwitch(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const GridItem(
|
||||||
|
crossAxisCellCount: 4,
|
||||||
|
child: OutboundMode(),
|
||||||
|
),
|
||||||
|
const GridItem(
|
||||||
|
crossAxisCellCount: 4,
|
||||||
|
child: NetworkDetection(),
|
||||||
|
),
|
||||||
|
const GridItem(
|
||||||
|
crossAxisCellCount: 4,
|
||||||
|
child: TrafficUsage(),
|
||||||
|
),
|
||||||
|
const GridItem(
|
||||||
|
crossAxisCellCount: 4,
|
||||||
|
child: IntranetIP(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onSave: (girdItems) {
|
|
||||||
final dashboardWidgets = girdItems
|
|
||||||
.map(
|
|
||||||
(item) => DashboardWidget.getDashboardWidget(item),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
dashboardWidgets: dashboardWidgets,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
addedItemsBuilder: (girdItems) {
|
|
||||||
return DashboardWidget.values
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
!girdItems.contains(item.widget) &&
|
|
||||||
item.platforms.contains(
|
|
||||||
SupportPlatform.currentPlatform,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((item) => item.widget)
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
130
lib/fragments/dashboard/intranet_ip.dart
Normal file
130
lib/fragments/dashboard/intranet_ip.dart
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class IntranetIP extends StatefulWidget {
|
||||||
|
const IntranetIP({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<IntranetIP> createState() => _IntranetIPState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntranetIPState extends State<IntranetIP> {
|
||||||
|
final ipNotifier = ValueNotifier<String?>("");
|
||||||
|
|
||||||
|
Future<String> getNetworkType() async {
|
||||||
|
try {
|
||||||
|
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||||
|
includeLoopback: false,
|
||||||
|
type: InternetAddressType.any,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var interface in interfaces) {
|
||||||
|
if (interface.name.toLowerCase().contains('wlan') ||
|
||||||
|
interface.name.toLowerCase().contains('wi-fi')) {
|
||||||
|
return 'WiFi';
|
||||||
|
}
|
||||||
|
if (interface.name.toLowerCase().contains('rmnet') ||
|
||||||
|
interface.name.toLowerCase().contains('ccmni') ||
|
||||||
|
interface.name.toLowerCase().contains('cellular')) {
|
||||||
|
return 'Mobile Data';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
|
} catch (e) {
|
||||||
|
return 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLocalIpAddress() async {
|
||||||
|
List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||||
|
includeLoopback: false,
|
||||||
|
)
|
||||||
|
..sort((a, b) {
|
||||||
|
if (a.isWifi && !b.isWifi) return -1;
|
||||||
|
if (!a.isWifi && b.isWifi) return 1;
|
||||||
|
if (a.includesIPv4 && !b.includesIPv4) return -1;
|
||||||
|
if (!a.includesIPv4 && b.includesIPv4) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
for (final interface in interfaces) {
|
||||||
|
final addresses = interface.addresses;
|
||||||
|
if (addresses.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addresses.sort((a, b) {
|
||||||
|
if (a.isIPv4 && !b.isIPv4) return -1;
|
||||||
|
if (!a.isIPv4 && b.isIPv4) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return addresses.first.address;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
ipNotifier.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
ipNotifier.value = await getLocalIpAddress() ?? "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonCard(
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.intranetIP,
|
||||||
|
iconData: Icons.devices,
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||||
|
height: globalState.measure.titleMediumHeight + 24 - 2,
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: ipNotifier,
|
||||||
|
builder: (_, value, __) {
|
||||||
|
return FadeBox(
|
||||||
|
child: value != null
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TooltipText(
|
||||||
|
text: Text(
|
||||||
|
value.isNotEmpty ? value : appLocalizations.noNetwork,
|
||||||
|
style: context
|
||||||
|
.textTheme.titleLarge?.toSoftBold.toMinus,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const Padding(
|
||||||
|
padding: EdgeInsets.all(2),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
228
lib/fragments/dashboard/network_detection.dart
Normal file
228
lib/fragments/dashboard/network_detection.dart
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
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 NetworkDetection extends StatefulWidget {
|
||||||
|
const NetworkDetection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NetworkDetection> createState() => _NetworkDetectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NetworkDetectionState extends State<NetworkDetection> {
|
||||||
|
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||||
|
const NetworkDetectionState(
|
||||||
|
isTesting: true,
|
||||||
|
ipInfo: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
bool? _preIsStart;
|
||||||
|
Function? _checkIpDebounce;
|
||||||
|
Timer? _setTimeoutTimer;
|
||||||
|
CancelToken? cancelToken;
|
||||||
|
|
||||||
|
_checkIp() async {
|
||||||
|
final appState = globalState.appController.appState;
|
||||||
|
final appFlowingState = globalState.appController.appFlowingState;
|
||||||
|
final isInit = appState.isInit;
|
||||||
|
if (!isInit) return;
|
||||||
|
final isStart = appFlowingState.isStart;
|
||||||
|
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||||
|
_clearSetTimeoutTimer();
|
||||||
|
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||||
|
isTesting: true,
|
||||||
|
ipInfo: null,
|
||||||
|
);
|
||||||
|
_preIsStart = isStart;
|
||||||
|
if (cancelToken != null) {
|
||||||
|
cancelToken!.cancel();
|
||||||
|
cancelToken = null;
|
||||||
|
}
|
||||||
|
cancelToken = CancelToken();
|
||||||
|
try {
|
||||||
|
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
||||||
|
if (ipInfo != null) {
|
||||||
|
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||||
|
isTesting: false,
|
||||||
|
ipInfo: ipInfo,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
|
||||||
|
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||||
|
isTesting: false,
|
||||||
|
ipInfo: null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
networkDetectionState.value = networkDetectionState.value.copyWith(
|
||||||
|
isTesting: true,
|
||||||
|
ipInfo: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearSetTimeoutTimer() {
|
||||||
|
if(_setTimeoutTimer != null){
|
||||||
|
_setTimeoutTimer?.cancel();
|
||||||
|
_setTimeoutTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkIpContainer(Widget child) {
|
||||||
|
return Selector<AppState, num>(
|
||||||
|
selector: (_, appState) {
|
||||||
|
return appState.checkIpNum;
|
||||||
|
},
|
||||||
|
builder: (_, checkIpNum, child) {
|
||||||
|
if (_checkIpDebounce != null) {
|
||||||
|
_checkIpDebounce!();
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
networkDetectionState.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String countryCodeToEmoji(String countryCode) {
|
||||||
|
final String code = countryCode.toUpperCase();
|
||||||
|
if (code.length != 2) {
|
||||||
|
return countryCode;
|
||||||
|
}
|
||||||
|
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
|
||||||
|
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
|
||||||
|
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_checkIpDebounce ??= debounce(_checkIp);
|
||||||
|
return _checkIpContainer(
|
||||||
|
ValueListenableBuilder<NetworkDetectionState>(
|
||||||
|
valueListenable: networkDetectionState,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final ipInfo = state.ipInfo;
|
||||||
|
final isTesting = state.isTesting;
|
||||||
|
return CommonCard(
|
||||||
|
onPressed: () {},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.network_check,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: FadeBox(
|
||||||
|
child: isTesting
|
||||||
|
? Text(
|
||||||
|
appLocalizations.checking,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium,
|
||||||
|
)
|
||||||
|
: ipInfo != null
|
||||||
|
? Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
height: globalState
|
||||||
|
.measure.titleMediumHeight,
|
||||||
|
child: Text(
|
||||||
|
countryCodeToEmoji(
|
||||||
|
ipInfo.countryCode),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleLarge
|
||||||
|
?.copyWith(
|
||||||
|
fontFamily: Fonts.twEmoji,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
appLocalizations.checkError,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: globalState.measure.titleLargeHeight + 24 - 2,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||||
|
child: FadeBox(
|
||||||
|
child: ipInfo != null
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TooltipText(
|
||||||
|
text: Text(
|
||||||
|
ipInfo.ip,
|
||||||
|
style: context.textTheme.titleLarge
|
||||||
|
?.toSoftBold.toMinus,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: FadeBox(
|
||||||
|
child: isTesting == false && ipInfo == null
|
||||||
|
? Text(
|
||||||
|
"timeout",
|
||||||
|
style: context.textTheme.titleLarge
|
||||||
|
?.copyWith(color: Colors.red)
|
||||||
|
.toSoftBold
|
||||||
|
.toMinus,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
child: const AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
166
lib/fragments/dashboard/network_speed.dart
Normal file
166
lib/fragments/dashboard/network_speed.dart
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class NetworkSpeed extends StatefulWidget {
|
||||||
|
const NetworkSpeed({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NetworkSpeed> createState() => _NetworkSpeedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||||
|
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
|
||||||
|
|
||||||
|
List<Point> _getPoints(List<Traffic> traffics) {
|
||||||
|
List<Point> trafficPoints = traffics
|
||||||
|
.toList()
|
||||||
|
.asMap()
|
||||||
|
.map(
|
||||||
|
(index, e) => MapEntry(
|
||||||
|
index,
|
||||||
|
Point(
|
||||||
|
(index + initPoints.length).toDouble(),
|
||||||
|
e.speed.toDouble(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.values
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return [...initPoints, ...trafficPoints];
|
||||||
|
}
|
||||||
|
|
||||||
|
Traffic _getLastTraffic(List<Traffic> traffics) {
|
||||||
|
if (traffics.isEmpty) return Traffic();
|
||||||
|
return traffics.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getLabel({
|
||||||
|
required String label,
|
||||||
|
required IconData iconData,
|
||||||
|
required TrafficValue value,
|
||||||
|
}) {
|
||||||
|
final showValue = value.showValue;
|
||||||
|
final showUnit = "${value.showUnit}/s";
|
||||||
|
final titleLargeSoftBold =
|
||||||
|
Theme.of(context).textTheme.titleLarge?.toSoftBold;
|
||||||
|
final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight;
|
||||||
|
final valueText = Text(
|
||||||
|
showValue,
|
||||||
|
style: titleLargeSoftBold,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
final unitText = Text(
|
||||||
|
showUnit,
|
||||||
|
style: bodyMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
final size = globalState.measure.computeTextSize(valueText);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Icon(iconData),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.toSoftBold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
child: OverflowBox(
|
||||||
|
maxWidth: 156,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: valueText,
|
||||||
|
),
|
||||||
|
const Flexible(
|
||||||
|
flex: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: unitText,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonCard(
|
||||||
|
onPressed: () {},
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.networkSpeed,
|
||||||
|
iconData: Icons.speed_sharp,
|
||||||
|
),
|
||||||
|
child: Selector<AppFlowingState, List<Traffic>>(
|
||||||
|
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||||
|
builder: (_, traffics, __) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 0,
|
||||||
|
child: LineChart(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
points: _getPoints(traffics),
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Flexible(child: SizedBox(height: 16)),
|
||||||
|
Flexible(
|
||||||
|
flex: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _getLabel(
|
||||||
|
iconData: Icons.upload,
|
||||||
|
label: appLocalizations.upload,
|
||||||
|
value: _getLastTraffic(traffics).up,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _getLabel(
|
||||||
|
iconData: Icons.download,
|
||||||
|
label: appLocalizations.download,
|
||||||
|
value: _getLastTraffic(traffics).down,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/fragments/dashboard/outbound_mode.dart
Normal file
69
lib/fragments/dashboard/outbound_mode.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class OutboundMode extends StatelessWidget {
|
||||||
|
const OutboundMode({super.key});
|
||||||
|
|
||||||
|
_changeMode(BuildContext context, Mode? value) async {
|
||||||
|
final appController = globalState.appController;
|
||||||
|
final clashConfig = appController.clashConfig;
|
||||||
|
if (value == null || clashConfig.mode == value) return;
|
||||||
|
clashConfig.mode = value;
|
||||||
|
appController.addCheckIpNumDebounce();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<ClashConfig, Mode>(
|
||||||
|
selector: (_, clashConfig) => clashConfig.mode,
|
||||||
|
builder: (_, mode, __) {
|
||||||
|
return CommonCard(
|
||||||
|
onPressed: () {},
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.outboundMode,
|
||||||
|
iconData: Icons.call_split_sharp,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (final item in Mode.values)
|
||||||
|
ListItem.radio(
|
||||||
|
horizontalTitleGap: 4,
|
||||||
|
prue: true,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
right: 16,
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
delegate: RadioDelegate(
|
||||||
|
value: item,
|
||||||
|
groupValue: mode,
|
||||||
|
onChanged: (value) async {
|
||||||
|
_changeMode(context, value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
Intl.message(item.name),
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,9 @@ class _StartButtonState extends State<StartButton>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
isStart = globalState.appController.appFlowingState.isStart;
|
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
value: isStart ? 1 : 0,
|
value: 0,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,58 +85,58 @@ class _StartButtonState extends State<StartButton>
|
|||||||
)
|
)
|
||||||
.width +
|
.width +
|
||||||
16;
|
16;
|
||||||
return _updateControllerContainer(
|
return AnimatedBuilder(
|
||||||
AnimatedBuilder(
|
animation: _controller.view,
|
||||||
animation: _controller.view,
|
builder: (_, child) {
|
||||||
builder: (_, child) {
|
return SizedBox(
|
||||||
return SizedBox(
|
width: 56 + textWidth * _controller.value,
|
||||||
width: 56 + textWidth * _controller.value,
|
height: 56,
|
||||||
height: 56,
|
child: FloatingActionButton(
|
||||||
child: FloatingActionButton(
|
heroTag: null,
|
||||||
heroTag: null,
|
onPressed: () {
|
||||||
onPressed: () {
|
handleSwitchStart();
|
||||||
handleSwitchStart();
|
},
|
||||||
},
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
width: 56,
|
||||||
width: 56,
|
height: 56,
|
||||||
height: 56,
|
alignment: Alignment.center,
|
||||||
alignment: Alignment.center,
|
child: AnimatedIcon(
|
||||||
child: AnimatedIcon(
|
icon: AnimatedIcons.play_pause,
|
||||||
icon: AnimatedIcons.play_pause,
|
progress: _controller,
|
||||||
progress: _controller,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: ClipRect(
|
Expanded(
|
||||||
child: OverflowBox(
|
child: ClipRect(
|
||||||
maxWidth: textWidth,
|
child: OverflowBox(
|
||||||
child: Container(
|
maxWidth: textWidth,
|
||||||
alignment: Alignment.centerLeft,
|
child: Container(
|
||||||
child: child!,
|
alignment: Alignment.centerLeft,
|
||||||
),
|
child: child!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
child: child,
|
},
|
||||||
),
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Selector<AppFlowingState, int?>(
|
child: _updateControllerContainer(
|
||||||
selector: (_, appFlowingState) => appFlowingState.runTime,
|
Selector<AppFlowingState, int?>(
|
||||||
builder: (_, int? value, __) {
|
selector: (_, appFlowingState) => appFlowingState.runTime,
|
||||||
final text = other.getTimeText(value);
|
builder: (_, int? value, __) {
|
||||||
return Text(
|
final text = other.getTimeText(value);
|
||||||
text,
|
return Text(
|
||||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
text,
|
||||||
);
|
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
125
lib/fragments/dashboard/status_switch.dart
Normal file
125
lib/fragments/dashboard/status_switch.dart
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import 'package:fl_clash/common/app_localizations.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 SwitchContainer(
|
||||||
|
// info: const Info(
|
||||||
|
// label: "VPN",
|
||||||
|
// iconData: Icons.stacked_line_chart,
|
||||||
|
// ),
|
||||||
|
// child: Selector<Config, bool>(
|
||||||
|
// selector: (_, config) => config.vpnProps.enable,
|
||||||
|
// builder: (_, enable, __) {
|
||||||
|
// return Switch(
|
||||||
|
// materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
// value: enable,
|
||||||
|
// onChanged: (value) {
|
||||||
|
// final config = globalState.appController.config;
|
||||||
|
// config.vpnProps = config.vpnProps.copyWith(
|
||||||
|
// enable: value,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
class TUNSwitch extends StatelessWidget {
|
||||||
|
const TUNSwitch({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SwitchContainer(
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.tun,
|
||||||
|
iconData: Icons.stacked_line_chart,
|
||||||
|
),
|
||||||
|
child: Selector<ClashConfig, bool>(
|
||||||
|
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||||
|
builder: (_, enable, __) {
|
||||||
|
return LocaleBuilder(
|
||||||
|
builder: (_) => Switch(
|
||||||
|
value: enable,
|
||||||
|
onChanged: (value) {
|
||||||
|
final clashConfig = globalState.appController.clashConfig;
|
||||||
|
clashConfig.tun = clashConfig.tun.copyWith(
|
||||||
|
enable: value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProxySwitch extends StatelessWidget {
|
||||||
|
const ProxySwitch({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SwitchContainer(
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.systemProxy,
|
||||||
|
iconData: Icons.shuffle,
|
||||||
|
),
|
||||||
|
child: Selector<Config, bool>(
|
||||||
|
selector: (_, config) => config.desktopProps.systemProxy,
|
||||||
|
builder: (_, systemProxy, __) {
|
||||||
|
return LocaleBuilder(
|
||||||
|
builder: (_) => Switch(
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
value: systemProxy,
|
||||||
|
onChanged: (value) {
|
||||||
|
final config = globalState.appController.config;
|
||||||
|
config.desktopProps =
|
||||||
|
config.desktopProps.copyWith(systemProxy: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SwitchContainer extends StatelessWidget {
|
||||||
|
final Info info;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const SwitchContainer({
|
||||||
|
super.key,
|
||||||
|
required this.info,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonCard(
|
||||||
|
onPressed: () {},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InfoHeader(
|
||||||
|
info: info,
|
||||||
|
actions: [
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/fragments/dashboard/traffic_usage.dart
Normal file
95
lib/fragments/dashboard/traffic_usage.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class TrafficUsage extends StatelessWidget {
|
||||||
|
const TrafficUsage({super.key});
|
||||||
|
|
||||||
|
Widget getTrafficDataItem(
|
||||||
|
BuildContext context,
|
||||||
|
IconData iconData,
|
||||||
|
TrafficValue trafficValue,
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
iconData,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: Text(
|
||||||
|
trafficValue.showValue,
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(fontSize: 18),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
trafficValue.showUnit,
|
||||||
|
style: context.textTheme.labelMedium?.toLight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonCard(
|
||||||
|
onPressed: () {},
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.trafficUsage,
|
||||||
|
iconData: Icons.data_saver_off,
|
||||||
|
),
|
||||||
|
child: Selector<AppFlowingState, Traffic>(
|
||||||
|
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
||||||
|
builder: (_, totalTraffic, __) {
|
||||||
|
final upTotalTrafficValue = totalTraffic.up;
|
||||||
|
final downTotalTrafficValue = totalTraffic.down;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: getTrafficDataItem(
|
||||||
|
context,
|
||||||
|
Icons.arrow_upward,
|
||||||
|
upTotalTrafficValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: getTrafficDataItem(
|
||||||
|
context,
|
||||||
|
Icons.arrow_downward,
|
||||||
|
downTotalTrafficValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/app.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class IntranetIP extends StatelessWidget {
|
|
||||||
const IntranetIP({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: getWidgetHeight(1),
|
|
||||||
child: CommonCard(
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.intranetIP,
|
|
||||||
iconData: Icons.devices,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
child: Container(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
top: 0,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: globalState.measure.bodyMediumHeight + 2,
|
|
||||||
child: Selector<AppFlowingState, String?>(
|
|
||||||
selector: (_, appFlowingState) => appFlowingState.localIp,
|
|
||||||
builder: (_, value, __) {
|
|
||||||
return FadeBox(
|
|
||||||
child: value != null
|
|
||||||
? TooltipText(
|
|
||||||
text: Text(
|
|
||||||
value.isNotEmpty
|
|
||||||
? value
|
|
||||||
: appLocalizations.noNetwork,
|
|
||||||
style: context.textTheme.bodyMedium?.toLight
|
|
||||||
.adjustSize(1),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
padding: EdgeInsets.all(2),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/common.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
final _memoryInfoStateNotifier =
|
|
||||||
ValueNotifier<TrafficValue>(TrafficValue(value: 0));
|
|
||||||
|
|
||||||
class MemoryInfo extends StatefulWidget {
|
|
||||||
const MemoryInfo({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MemoryInfo> createState() => _MemoryInfoState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MemoryInfoState extends State<MemoryInfo> {
|
|
||||||
Timer? timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
clashCore.getMemory().then((memory) {
|
|
||||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
|
||||||
});
|
|
||||||
_updateMemoryData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
timer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateMemoryData() {
|
|
||||||
timer = Timer(Duration(seconds: 2), () async {
|
|
||||||
final memory = await clashCore.getMemory();
|
|
||||||
_memoryInfoStateNotifier.value = TrafficValue(value: memory);
|
|
||||||
_updateMemoryData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: getWidgetHeight(2),
|
|
||||||
child: CommonCard(
|
|
||||||
info: Info(
|
|
||||||
iconData: Icons.memory,
|
|
||||||
label: appLocalizations.memoryInfo,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
child: ValueListenableBuilder(
|
|
||||||
valueListenable: _memoryInfoStateNotifier,
|
|
||||||
builder: (_, trafficValue, __) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
bottom: 0,
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
trafficValue.showValue,
|
|
||||||
style: context.textTheme.titleLarge?.toLight,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
trafficValue.showUnit,
|
|
||||||
style: context.textTheme.titleLarge?.toLight,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: WaveView(
|
|
||||||
waveAmplitude: 12.0,
|
|
||||||
waveFrequency: 0.35,
|
|
||||||
waveColor: context.colorScheme.secondaryContainer
|
|
||||||
.blendDarken(context, factor: 0.1)
|
|
||||||
.toLighter,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fill(
|
|
||||||
child: WaveView(
|
|
||||||
waveAmplitude: 12.0,
|
|
||||||
waveFrequency: 0.9,
|
|
||||||
waveColor: context.colorScheme.secondaryContainer
|
|
||||||
.blendDarken(context, factor: 0.1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
|
||||||
const NetworkDetectionState(
|
|
||||||
isTesting: true,
|
|
||||||
ipInfo: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
class NetworkDetection extends StatefulWidget {
|
|
||||||
const NetworkDetection({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<NetworkDetection> createState() => _NetworkDetectionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
|
||||||
bool? _preIsStart;
|
|
||||||
Timer? _setTimeoutTimer;
|
|
||||||
CancelToken? cancelToken;
|
|
||||||
Completer? checkedCompleter;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
_startCheck() async {
|
|
||||||
await checkedCompleter?.future;
|
|
||||||
if (cancelToken != null) {
|
|
||||||
cancelToken!.cancel();
|
|
||||||
cancelToken = null;
|
|
||||||
}
|
|
||||||
debouncer.call(
|
|
||||||
DebounceTag.checkIp,
|
|
||||||
_checkIp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkIp() async {
|
|
||||||
final appState = globalState.appController.appState;
|
|
||||||
final appFlowingState = globalState.appController.appFlowingState;
|
|
||||||
final isInit = appState.isInit;
|
|
||||||
if (!isInit) return;
|
|
||||||
final isStart = appFlowingState.isStart;
|
|
||||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
|
||||||
_clearSetTimeoutTimer();
|
|
||||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
|
||||||
isTesting: true,
|
|
||||||
ipInfo: null,
|
|
||||||
);
|
|
||||||
_preIsStart = isStart;
|
|
||||||
if (cancelToken != null) {
|
|
||||||
cancelToken!.cancel();
|
|
||||||
cancelToken = null;
|
|
||||||
}
|
|
||||||
cancelToken = CancelToken();
|
|
||||||
try {
|
|
||||||
final ipInfo = await request.checkIp(cancelToken: cancelToken);
|
|
||||||
if (ipInfo != null) {
|
|
||||||
checkedCompleter = Completer();
|
|
||||||
checkedCompleter?.complete(
|
|
||||||
Future.delayed(
|
|
||||||
Duration(milliseconds: 3000),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
|
||||||
isTesting: false,
|
|
||||||
ipInfo: ipInfo,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_clearSetTimeoutTimer();
|
|
||||||
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
|
|
||||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
|
||||||
isTesting: false,
|
|
||||||
ipInfo: null,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e.toString() == "cancelled") {
|
|
||||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
|
||||||
isTesting: true,
|
|
||||||
ipInfo: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_clearSetTimeoutTimer();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_clearSetTimeoutTimer() {
|
|
||||||
if (_setTimeoutTimer != null) {
|
|
||||||
_setTimeoutTimer?.cancel();
|
|
||||||
_setTimeoutTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_checkIpContainer(Widget child) {
|
|
||||||
return Selector<AppState, num>(
|
|
||||||
selector: (_, appState) {
|
|
||||||
return appState.checkIpNum;
|
|
||||||
},
|
|
||||||
shouldRebuild: (prev, next) {
|
|
||||||
if (prev != next) {
|
|
||||||
_startCheck();
|
|
||||||
}
|
|
||||||
return prev != next;
|
|
||||||
},
|
|
||||||
builder: (_, checkIpNum, child) {
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_countryCodeToEmoji(String countryCode) {
|
|
||||||
final String code = countryCode.toUpperCase();
|
|
||||||
if (code.length != 2) {
|
|
||||||
return countryCode;
|
|
||||||
}
|
|
||||||
final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6;
|
|
||||||
final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6;
|
|
||||||
return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: getWidgetHeight(1),
|
|
||||||
child: _checkIpContainer(
|
|
||||||
ValueListenableBuilder<NetworkDetectionState>(
|
|
||||||
valueListenable: _networkDetectionState,
|
|
||||||
builder: (_, state, __) {
|
|
||||||
final ipInfo = state.ipInfo;
|
|
||||||
final isTesting = state.isTesting;
|
|
||||||
return CommonCard(
|
|
||||||
onPressed: () {},
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: globalState.measure.titleMediumHeight + 16,
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
bottom: 0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
ipInfo != null
|
|
||||||
? Text(
|
|
||||||
_countryCodeToEmoji(
|
|
||||||
ipInfo.countryCode,
|
|
||||||
),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleMedium
|
|
||||||
?.toLight
|
|
||||||
.copyWith(
|
|
||||||
fontFamily: FontFamily.twEmoji.value,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Icons.network_check,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TooltipText(
|
|
||||||
text: Text(
|
|
||||||
appLocalizations.networkDetection,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: context.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
top: 0,
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
height: globalState.measure.bodyMediumHeight + 2,
|
|
||||||
child: FadeBox(
|
|
||||||
child: ipInfo != null
|
|
||||||
? TooltipText(
|
|
||||||
text: Text(
|
|
||||||
ipInfo.ip,
|
|
||||||
style: context.textTheme.bodyMedium?.toLight
|
|
||||||
.adjustSize(1),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: FadeBox(
|
|
||||||
child: isTesting == false && ipInfo == null
|
|
||||||
? Text(
|
|
||||||
"timeout",
|
|
||||||
style: context.textTheme.bodyMedium
|
|
||||||
?.copyWith(color: Colors.red)
|
|
||||||
.adjustSize(1),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
child: const AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class NetworkSpeed extends StatefulWidget {
|
|
||||||
const NetworkSpeed({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<NetworkSpeed> createState() => _NetworkSpeedState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NetworkSpeedState extends State<NetworkSpeed> {
|
|
||||||
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
|
|
||||||
|
|
||||||
List<Point> _getPoints(List<Traffic> traffics) {
|
|
||||||
List<Point> trafficPoints = traffics
|
|
||||||
.toList()
|
|
||||||
.asMap()
|
|
||||||
.map(
|
|
||||||
(index, e) => MapEntry(
|
|
||||||
index,
|
|
||||||
Point(
|
|
||||||
(index + initPoints.length).toDouble(),
|
|
||||||
e.speed.toDouble(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.values
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return [...initPoints, ...trafficPoints];
|
|
||||||
}
|
|
||||||
|
|
||||||
Traffic _getLastTraffic(List<Traffic> traffics) {
|
|
||||||
if (traffics.isEmpty) return Traffic();
|
|
||||||
return traffics.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final color = context.colorScheme.onSurfaceVariant.toLight;
|
|
||||||
return SizedBox(
|
|
||||||
height: getWidgetHeight(2),
|
|
||||||
child: CommonCard(
|
|
||||||
onPressed: () {},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.networkSpeed,
|
|
||||||
iconData: Icons.speed_sharp,
|
|
||||||
),
|
|
||||||
child: Selector<AppFlowingState, List<Traffic>>(
|
|
||||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
|
||||||
builder: (_, traffics, __) {
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(16).copyWith(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
),
|
|
||||||
child: LineChart(
|
|
||||||
gradient: true,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
points: _getPoints(traffics),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
-16,
|
|
||||||
-20,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_upward,
|
|
||||||
color: color,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${_getLastTraffic(traffics).up}/s",
|
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_downward,
|
|
||||||
color: color,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${_getLastTraffic(traffics).down}/s",
|
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class OutboundMode extends StatelessWidget {
|
|
||||||
const OutboundMode({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final height = getWidgetHeight(2);
|
|
||||||
return SizedBox(
|
|
||||||
height: height,
|
|
||||||
child: Selector<ClashConfig, Mode>(
|
|
||||||
selector: (_, clashConfig) => clashConfig.mode,
|
|
||||||
builder: (_, mode, __) {
|
|
||||||
return CommonCard(
|
|
||||||
onPressed: () {},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.outboundMode,
|
|
||||||
iconData: Icons.call_split_sharp,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 12,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
for (final item in Mode.values)
|
|
||||||
Flexible(
|
|
||||||
child: ListItem.radio(
|
|
||||||
prue: true,
|
|
||||||
horizontalTitleGap: 4,
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 12,
|
|
||||||
right: 16,
|
|
||||||
),
|
|
||||||
delegate: RadioDelegate(
|
|
||||||
value: item,
|
|
||||||
groupValue: mode,
|
|
||||||
onChanged: (value) async {
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
globalState.appController.changeMode(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
Intl.message(item.name),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.toSoftBold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/fragments/config/network.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class TUNButton extends StatelessWidget {
|
|
||||||
const TUNButton({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return LocaleBuilder(
|
|
||||||
builder: (_) => SizedBox(
|
|
||||||
height: getWidgetHeight(1),
|
|
||||||
child: CommonCard(
|
|
||||||
onPressed: () {
|
|
||||||
showSheet(
|
|
||||||
context: context,
|
|
||||||
body: generateListView(generateSection(
|
|
||||||
items: [
|
|
||||||
if (system.isDesktop) const TUNItem(),
|
|
||||||
const TunStackItem(),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
title: appLocalizations.tun,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.tun,
|
|
||||||
iconData: Icons.stacked_line_chart,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
top: 4,
|
|
||||||
bottom: 8,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TooltipText(
|
|
||||||
text: Text(
|
|
||||||
appLocalizations.options,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleSmall
|
|
||||||
?.adjustSize(-2)
|
|
||||||
.toLight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Selector<ClashConfig, bool>(
|
|
||||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
|
||||||
builder: (_, enable, __) {
|
|
||||||
return Switch(
|
|
||||||
value: enable,
|
|
||||||
onChanged: (value) {
|
|
||||||
final clashConfig =
|
|
||||||
globalState.appController.clashConfig;
|
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
|
||||||
enable: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SystemProxyButton extends StatelessWidget {
|
|
||||||
const SystemProxyButton({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: getWidgetHeight(1),
|
|
||||||
child: LocaleBuilder(
|
|
||||||
builder: (_) => CommonCard(
|
|
||||||
onPressed: () {
|
|
||||||
showSheet(
|
|
||||||
context: context,
|
|
||||||
body: generateListView(
|
|
||||||
generateSection(
|
|
||||||
items: [
|
|
||||||
SystemProxyItem(),
|
|
||||||
BypassDomainItem(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: appLocalizations.systemProxy,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.systemProxy,
|
|
||||||
iconData: Icons.shuffle,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
top: 4,
|
|
||||||
bottom: 8,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TooltipText(
|
|
||||||
text: Text(
|
|
||||||
appLocalizations.options,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleSmall
|
|
||||||
?.adjustSize(-2)
|
|
||||||
.toLight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.networkProps.systemProxy,
|
|
||||||
builder: (_, systemProxy, __) {
|
|
||||||
return Switch(
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
value: systemProxy,
|
|
||||||
onChanged: (value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.networkProps =
|
|
||||||
config.networkProps.copyWith(systemProxy: value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class TrafficUsage extends StatelessWidget {
|
|
||||||
const TrafficUsage({super.key});
|
|
||||||
|
|
||||||
Widget getTrafficDataItem(
|
|
||||||
BuildContext context,
|
|
||||||
Icon icon,
|
|
||||||
TrafficValue trafficValue,
|
|
||||||
) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
icon,
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Text(
|
|
||||||
trafficValue.showValue,
|
|
||||||
style: context.textTheme.bodySmall,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
trafficValue.showUnit,
|
|
||||||
style: context.textTheme.bodySmall?.toLighter,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final primaryColor =
|
|
||||||
context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2);
|
|
||||||
final secondaryColor =
|
|
||||||
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3);
|
|
||||||
return SizedBox(
|
|
||||||
height: getWidgetHeight(2),
|
|
||||||
child: CommonCard(
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.trafficUsage,
|
|
||||||
iconData: Icons.data_saver_off,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
child: Selector<AppFlowingState, Traffic>(
|
|
||||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
|
||||||
builder: (_, totalTraffic, __) {
|
|
||||||
final upTotalTrafficValue = totalTraffic.up;
|
|
||||||
final downTotalTrafficValue = totalTraffic.down;
|
|
||||||
return Padding(
|
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
|
||||||
top: 0,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: DonutChart(
|
|
||||||
data: [
|
|
||||||
DonutChartData(
|
|
||||||
value: upTotalTrafficValue.value.toDouble(),
|
|
||||||
color: primaryColor,
|
|
||||||
),
|
|
||||||
DonutChartData(
|
|
||||||
value: downTotalTrafficValue.value.toDouble(),
|
|
||||||
color: secondaryColor,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (_, container) {
|
|
||||||
final uploadText = Text(
|
|
||||||
maxLines: 1,
|
|
||||||
appLocalizations.upload,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodySmall,
|
|
||||||
);
|
|
||||||
final downloadText = Text(
|
|
||||||
maxLines: 1,
|
|
||||||
appLocalizations.download,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodySmall,
|
|
||||||
);
|
|
||||||
final uploadTextSize = globalState.measure
|
|
||||||
.computeTextSize(uploadText);
|
|
||||||
final downloadTextSize = globalState.measure
|
|
||||||
.computeTextSize(downloadText);
|
|
||||||
final maxTextWidth = max(uploadTextSize.width,
|
|
||||||
downloadTextSize.width);
|
|
||||||
if (maxTextWidth + 24 > container.maxWidth) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 20,
|
|
||||||
height: 8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: primaryColor,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
maxLines: 1,
|
|
||||||
appLocalizations.upload,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 20,
|
|
||||||
height: 8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: secondaryColor,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
maxLines: 1,
|
|
||||||
appLocalizations.download,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: context.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
getTrafficDataItem(
|
|
||||||
context,
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_upward,
|
|
||||||
color: primaryColor,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
upTotalTrafficValue,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
getTrafficDataItem(
|
|
||||||
context,
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_downward,
|
|
||||||
color: secondaryColor,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
downTotalTrafficValue,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export 'intranet_ip.dart';
|
|
||||||
export 'network_detection.dart';
|
|
||||||
export 'network_speed.dart';
|
|
||||||
export 'outbound_mode.dart';
|
|
||||||
export 'quick_options.dart';
|
|
||||||
export 'traffic_usage.dart';
|
|
||||||
export 'memory_info.dart';
|
|
||||||
@@ -49,11 +49,11 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
logsNotifier.dispose();
|
logsNotifier.dispose();
|
||||||
scrollController.dispose();
|
scrollController.dispose();
|
||||||
timer = null;
|
timer = null;
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleExport() async {
|
_handleExport() async {
|
||||||
@@ -87,6 +87,9 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_handleExport();
|
_handleExport();
|
||||||
@@ -232,8 +235,8 @@ class LogsSearchDelegate extends SearchDelegate {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
logsNotifier.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
logsNotifier.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
get state => logsNotifier.value;
|
get state => logsNotifier.value;
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
class AddProfile extends StatelessWidget {
|
class AddProfile extends StatelessWidget {
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
|
|
||||||
const AddProfile({
|
const AddProfile({super.key, required this.context,});
|
||||||
super.key,
|
|
||||||
required this.context,
|
|
||||||
});
|
|
||||||
|
|
||||||
_handleAddProfileFormFile() async {
|
_handleAddProfileFormFile() async {
|
||||||
globalState.appController.addProfileFormFile();
|
globalState.appController.addProfileFormFile();
|
||||||
@@ -21,16 +18,14 @@ class AddProfile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_toScan() async {
|
_toScan() async {
|
||||||
if (system.isDesktop) {
|
if(system.isDesktop){
|
||||||
globalState.appController.addProfileFormQrCode();
|
globalState.appController.addProfileFormQrCode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final url = await BaseNavigator.push(
|
final url = await Navigator.of(context)
|
||||||
context,
|
.push<String>(MaterialPageRoute(builder: (_) => const ScanPage()));
|
||||||
const ScanPage(),
|
|
||||||
);
|
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_){
|
||||||
_handleAddProfileFormURL(url);
|
_handleAddProfileFormURL(url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -49,12 +44,12 @@ class AddProfile extends StatelessWidget {
|
|||||||
Widget build(context) {
|
Widget build(context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
ListItem(
|
ListItem(
|
||||||
leading: const Icon(Icons.qr_code),
|
leading: const Icon(Icons.qr_code),
|
||||||
title: Text(appLocalizations.qrcode),
|
title: Text(appLocalizations.qrcode),
|
||||||
subtitle: Text(appLocalizations.qrcodeDesc),
|
subtitle: Text(appLocalizations.qrcodeDesc),
|
||||||
onTap: _toScan,
|
onTap: _toScan,
|
||||||
),
|
),
|
||||||
ListItem(
|
ListItem(
|
||||||
leading: const Icon(Icons.upload_file),
|
leading: const Icon(Icons.upload_file),
|
||||||
title: Text(appLocalizations.file),
|
title: Text(appLocalizations.file),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user