Compare commits

..

35 Commits

Author SHA1 Message Date
chen08209
8d4931c093 Fix windows admin auto launch issues
Add android vpn options

Support proxies icon configuration

Optimize android immersion display

Fix some issues
2024-10-08 09:21:21 +08:00
chen08209
23e3baf534 Optimize ip detection
Support android vpn ipv6 inbound switch

Support log export

Optimize more details
2024-09-26 14:27:52 +08:00
chen08209
d522a64604 Fix android system dns issues
Optimize dns default option

Fix some issues
2024-09-20 11:21:25 +08:00
chen08209
3902ea0064 Update readme 2024-09-18 00:42:20 +08:00
chen08209
5566f2b54f Update README.md 2 2024-09-18 00:22:31 +08:00
chen08209
511b7c521a Update README.md 2 2024-09-18 00:20:00 +08:00
chen08209
fb01d87371 Update README.md 2024-09-18 00:17:12 +08:00
chen08209
043648f998 Fix build error2 2024-09-17 23:06:32 +08:00
chen08209
3eb26e8061 Fix build error 2024-09-17 22:42:10 +08:00
chen08209
5d6bd6466f Support desktop hotkey
Support android ipv6 inbound

Support android system dns

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

Optimize tray

Update core

Fix some error
2024-09-08 20:58:02 +08:00
chen08209
79efa67df3 Fix tun update issues 2024-09-02 16:42:47 +08:00
chen08209
ac397393a0 Add DNS override
Fixed some bugs
Optimize more detail
2024-09-02 16:09:51 +08:00
chen08209
b685165230 Add Hosts override 2024-09-02 16:09:27 +08:00
chen08209
402221aaa2 fix android tip error
fix windows auto launch error
2024-08-26 20:35:38 +08:00
chen08209
f6d9ed11d9 Fix windows tray issues
Optimize windows logic
2024-08-25 23:40:13 +08:00
chen08209
c38a671d57 Optimize app logic
Support windows administrator auto launch

Support android close vpn
2024-08-22 19:56:19 +08:00
chen08209
75af47aead Change flutter version 2024-08-15 14:34:02 +08:00
chen08209
8dafe3b0ec Support profiles sort
Support windows country flags display

Optimize proxies page and profiles page columns
2024-08-15 14:18:33 +08:00
chen08209
813198a21d Update flutter version 2024-08-11 17:45:57 +08:00
chen08209
68dd262fef Update version 2024-08-11 17:09:31 +08:00
chen08209
5ef020db73 Update timeout time 2024-08-11 17:08:51 +08:00
chen08209
e3c9035903 Update access control page
Fix bug
2024-08-11 17:08:46 +08:00
chen08209
7fc54c5295 Optimize provider page
Optimize delay test

Support local backup and recovery
2024-08-05 18:17:05 +08:00
chen08209
00a78b5fb4 Fix android tile service issues 2024-08-01 23:51:28 +08:00
chen08209
8cdaf30de0 Fix linux core build error 2024-07-31 21:24:31 +08:00
chen08209
f39b9cf933 Add proxy-only traffic statistics
Update core

Optimize more details
2024-07-31 21:05:16 +08:00
chen08209
9df1ff46c2 Merge pull request #140 from txyyh/main
添加自建 F-Droid 仓库相关 workflow
2024-07-29 16:48:04 +08:00
txyyh
fcbbbdc698 Rename readme fingerprint 2024-07-29 16:45:12 +08:00
txyyh
3ba8355772 Rename workflow deploy repo name 2024-07-29 16:42:25 +08:00
txyyh
f6b97f82ae Add download guide to README 2024-07-29 16:39:52 +08:00
txyyh
13ac20f273 Add push release files to fdroid-repo 2024-07-29 16:39:52 +08:00
177 changed files with 8591 additions and 15226 deletions

View File

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

View File

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

View File

@@ -1,66 +0,0 @@
name: changelog
on:
push:
tags:
- 'v*'
jobs:
changelog:
if: ${{ !contains(github.ref, '+') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag=""
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then
tag=${tags[$i]}
else
tag=""
fi
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
break
fi
fi
if [ -n "$currentTag" ]; then
echo "## $currentTag" >> NEW_CHANGELOG.md
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done
cat CHANGELOG.md >> NEW_CHANGELOG.md
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
echo "Commit pushing"
git config --local user.email "chen08209@gmail.com"
git config --local user.name "chen08209"
git commit -m "Update changelog"
git push
if [ $? -eq 0 ]; then
echo "Push succeeded"
else
echo "Push failed"
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

@@ -10,8 +10,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -24,8 +23,8 @@
<application
android:name="${applicationName}"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:hardwareAccelerated="true"
android:label="FlClash">
<activity
android:name="com.follow.clash.MainActivity"
@@ -74,11 +73,11 @@
android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.STOP" />
<action android:name="com.follow.clash.action.START" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.CHANGE" />
<action android:name="com.follow.clash.action.STOP" />
</intent-filter>
</activity>

View File

@@ -4,8 +4,8 @@ import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
@@ -33,10 +33,6 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: ""
}
fun getCurrentTilePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
@@ -46,27 +42,6 @@ object GlobalState {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}
fun handleToggle(context: Context) {
if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING
val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
initServiceEngine(context)
}
} else {
handleStop()
}
}
fun handleStop() {
if (runState.value == RunState.START) {
runState.value = RunState.PENDING
getCurrentTilePlugin()?.handleStop()
}
}
fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null

View File

@@ -1,10 +1,12 @@
package com.follow.clash
import android.content.Intent
import android.os.Bundle
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

View File

@@ -2,18 +2,17 @@ package com.follow.clash
import android.app.Activity
import android.os.Bundle
import com.follow.clash.extensions.wrapAction
class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
wrapAction("STOP") -> {
GlobalState.handleStop()
"com.follow.clash.action.START" -> {
GlobalState.getCurrentTilePlugin()?.handleStart()
}
wrapAction("CHANGE") -> {
GlobalState.handleToggle(applicationContext)
"com.follow.clash.action.STOP" -> {
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}
finishAndRemoveTask()

View File

@@ -1,30 +1,21 @@
package com.follow.clash.extensions
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.TempActivity
import com.follow.clash.models.CIDR
import com.follow.clash.models.Metadata
import com.follow.clash.models.VpnOptions
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Drawable.getBase64(): String {
@@ -43,40 +34,6 @@ fun Metadata.getProtocol(): Int? {
return null
}
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv4()
}.map {
it.toCIDR()
}
}
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv6()
}.map {
it.toCIDR()
}
}
fun String.isIpv4(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 4
}
fun String.isIpv6(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 16
}
fun String.toCIDR(): CIDR {
val parts = split("/")
if (parts.size != 2) {
@@ -114,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 {
val sb = StringBuilder(39)
@@ -158,25 +87,3 @@ private fun numericToTextFormat(src: ByteArray): String {
}
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)
}
})
}
}

View File

@@ -3,7 +3,8 @@ package com.follow.clash.models
import java.net.InetAddress
enum class AccessControlMode {
acceptSelected, rejectSelected,
acceptSelected,
rejectSelected,
}
data class AccessControl(
@@ -21,7 +22,6 @@ data class VpnOptions(
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,
val routeAddress: List<String>,
val ipv4Address: String,
val ipv6Address: String,
val dnsServerAddress: String,

View File

@@ -14,15 +14,9 @@ import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider
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.models.Package
import com.google.gson.Gson
@@ -37,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.util.zip.ZipFile
@@ -123,21 +116,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
}
private fun initShortcuts(label: String) {
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
.setShortLabel(label)
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
.setIntent(context.getActionIntent("CHANGE"))
.build()
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel()
@@ -145,7 +128,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun tip(message: String?) {
if (GlobalState.flutterEngine == null) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
if (toast != null) {
toast!!.cancel()
}
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
}
}
@@ -153,18 +140,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
when (call.method) {
"moveTaskToBack" -> {
activity?.moveTaskToBack(true)
result.success(true)
result.success(true);
}
"updateExcludeFromRecents" -> {
val value = call.argument<Boolean>("value")
updateExcludeFromRecents(value)
result.success(true)
}
"initShortcuts" -> {
initShortcuts(call.arguments as String)
result.success(true)
result.success(true);
}
"getPackages" -> {
@@ -215,7 +197,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
else -> {
result.notImplemented()
result.notImplemented();
}
}
}
@@ -288,7 +270,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun getPackages(): List<Package> {
val packageManager = context.packageManager
if (packages.isNotEmpty()) return packages
if (packages.isNotEmpty()) return packages;
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
@@ -302,7 +284,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
firstInstallTime = it.firstInstallTime
)
}?.let { packages.addAll(it) }
return packages
return packages;
}
private suspend fun getPackagesToJson(): String {
@@ -324,7 +306,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return
return;
}
vpnCallBack?.invoke()
}
@@ -348,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 {
val packageManager = context.packageManager ?: return false
skipPrefixList.forEach {
@@ -422,7 +398,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
activity = binding.activity;
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
@@ -432,7 +408,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
activity = binding.activity;
}
override fun onDetachedFromActivity() {

View File

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

View File

@@ -13,13 +13,8 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlClashService : Service(), BaseServiceInterface {
@@ -69,11 +64,6 @@ class FlClashService : Service(), BaseServiceInterface {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
addAction(
0,
GlobalState.getText("stop"),
getActionPendingIntent("STOP")
)
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
@@ -92,23 +82,21 @@ class FlClashService : Service(), BaseServiceInterface {
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) {
CoroutineScope(Dispatchers.Default).launch {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
@@ -66,7 +67,19 @@ class FlClashTileService : TileService() {
override fun onClick() {
super.onClick()
activityTransfer()
GlobalState.handleToggle(applicationContext)
if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
val 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() {

View File

@@ -15,15 +15,12 @@ import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.extensions.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.TempActivity
import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions
@@ -43,28 +40,12 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i ->
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
}
} else {
addRoute("0.0.0.0", 0)
}
addRoute("0.0.0.0", 0)
}
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i ->
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
}
} else {
addRoute("::", 0)
}
addRoute("::", 0)
}
addDnsServer(options.dnsServerAddress)
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)) {
setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash")
@@ -151,40 +152,31 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0,
GlobalState.getText("stop"),
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
addAction(0, "Stop", stopPendingIntent);
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) {
CoroutineScope(Dispatchers.Default).launch {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder
.setContentTitle(title)
.setContentText(content)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
notificationBuilder.setContentTitle(title).setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
}
override fun onTrimMemory(level: Int) {

Binary file not shown.

View File

@@ -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
}

View File

@@ -1,10 +1,21 @@
package main
import "C"
import (
"context"
"encoding/json"
"core/state"
"errors"
"fmt"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/hub/route"
"math"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -14,28 +25,52 @@ import (
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider"
"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/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/samber/lo"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
)
var (
isRunning = false
runLock sync.Mutex
ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
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"`
}
type ExternalProviders []ExternalProvider
@@ -43,9 +78,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) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
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) {
@@ -60,6 +116,19 @@ func readFile(path string) ([]byte, error) {
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 {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
@@ -86,16 +155,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
if configParams.TestURL != nil {
if mapping["health-check"] != nil {
hc := mapping["health-check"].(map[string]any)
if hc != nil {
if hc["url"] != nil {
hc["url"] = *configParams.TestURL
}
}
}
}
}
for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string)
@@ -127,13 +186,12 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
return &ExternalProvider{
Name: psp.Name(),
Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
UpdateAt: psp.UpdatedAt(),
Path: psp.Vehicle().Path(),
SubscriptionInfo: psp.GetSubscriptionInfo(),
Name: psp.Name(),
Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
Path: psp.Vehicle().Path(),
UpdateAt: psp.UpdatedAt(),
}, nil
case *rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
@@ -142,8 +200,8 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
Type: rsp.Type().String(),
VehicleType: rsp.VehicleType().String(),
Count: rsp.Count(),
UpdateAt: rsp.UpdatedAt(),
Path: rsp.Vehicle().Path(),
UpdateAt: rsp.UpdatedAt(),
}, nil
default:
return nil, errors.New("not external provider")
@@ -154,16 +212,16 @@ func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
switch p.(type) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
_, _, err := psp.SideUpdate(bytes)
if err == nil {
return err
elm, same, err := psp.SideUpdate(bytes)
if err == nil && !same {
psp.OnUpdate(elm)
}
return nil
case rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
_, _, err := rsp.SideUpdate(bytes)
if err == nil {
return err
elm, same, err := rsp.SideUpdate(bytes)
if err == nil && !same {
rsp.OnUpdate(elm)
}
return nil
default:
@@ -177,41 +235,158 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof
}
//func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
// for _, v := range s {
// initVal = f(initVal, v)
// }
// return initVal
//}
//
//func Map[T, U any](slice []T, fn func(T) U) []U {
// result := make([]U, len(slice))
// for i, v := range slice {
// result[i] = fn(v)
// }
// return result
//}
//
//func replaceFromMap(s string, m map[string]string) string {
// for k, v := range m {
// s = strings.ReplaceAll(s, k, v)
// }
// return s
//}
//
//func removeDuplicateFromSlice[T any](slice []T) []T {
// result := make([]T, 0)
// seen := make(map[any]struct{})
// for _, value := range slice {
// if _, ok := seen[value]; !ok {
// result = append(result, value)
// seen[value] = struct{}{}
// }
// }
// return result
//}
//func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
// var replacements = map[string]string{}
// var selectArr []map[string]any
// var urlTestArr []map[string]any
// var fallbackArr []map[string]any
// for _, group := range *proxyGroup {
// switch group["type"] {
// case "select":
// selectArr = append(selectArr, group)
// replacements[group["name"].(string)] = "Proxy"
// break
// case "url-test":
// urlTestArr = append(urlTestArr, group)
// replacements[group["name"].(string)] = "Auto"
// break
// case "fallback":
// fallbackArr = append(fallbackArr, group)
// replacements[group["name"].(string)] = "Fallback"
// break
// default:
// break
// }
// }
//
// ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Proxy" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
//
// AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Auto" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// AutoProxies = removeDuplicateFromSlice(AutoProxies)
//
// FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Fallback" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
//
// var computedProxyGroup []map[string]any
//
// if len(ProxyProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Proxy",
// "type": "select",
// "proxies": ProxyProxies,
// })
// }
//
// if len(AutoProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Auto",
// "type": "url-test",
// "proxies": AutoProxies,
// })
// }
//
// if len(FallbackProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Fallback",
// "type": "fallback",
// "proxies": FallbackProxies,
// })
// }
//
// computedRule := Map(*rule, func(value string) string {
// return replaceFromMap(value, replacements)
// })
//
// *proxyGroup = computedProxyGroup
// *rule = computedRule
//}
func genHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
hosts[k] = v
}
}
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
rule := trimArr(strings.Split(line, ","))
l := len(rule)
if l != 2 {
return
}
if strings.ToUpper(rule[0]) == "MATCH" {
target = rule[1]
break
}
}
if target == "" {
return
}
var rulesExt = lo.Map(ips, func(ip string, index int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target)
})
*rules = append(rulesExt, *rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
@@ -236,12 +411,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
targetConfig.DNS = patchConfig.DNS
@@ -250,14 +419,21 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.DNS.Enable = true
}
}
overrideRules(&targetConfig.Rule)
//if runtime.GOOS == "android" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
//} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//}
//if configParams.IsCompatible == false {
// targetConfig.ProxyProvider = make(map[string]map[string]any)
// targetConfig.RuleProvider = make(map[string]map[string]any)
// generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
//}
}
func patchConfig() {
func patchConfig(general *config.General, controller *config.Controller) {
log.Infoln("[Apply] patch")
general := currentConfig.General
controller := currentConfig.Controller
tls := currentConfig.TLS
route.ReStartServer(controller.ExternalController)
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
@@ -266,33 +442,19 @@ func patchConfig() {
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
route.ReCreateServer(&route.Config{
Addr: controller.ExternalController,
TLSAddr: controller.ExternalControllerTLS,
UnixAddr: controller.ExternalControllerUnix,
PipeAddr: controller.ExternalControllerPipe,
Secret: controller.Secret,
Certificate: tls.Certificate,
PrivateKey: tls.PrivateKey,
DohServer: controller.ExternalDohServer,
IsDebug: false,
Cors: route.Cors{
AllowOrigins: controller.Cors.AllowOrigins,
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
},
})
}
func updateListeners(force bool) {
var isRunning = false
var runLock sync.Mutex
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
if !isRunning {
return
}
general := currentConfig.General
listeners := currentConfig.Listeners
if force == true {
stopListeners()
}
runLock.Lock()
defer runLock.Unlock()
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -316,6 +478,27 @@ func stopListeners() {
listener.StopListener()
}
func hcCompatibleProvider(proxyProviders map[string]cp.ProxyProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, math.MaxInt)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == cp.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
}
}()
}
}
}
func patchSelectGroup() {
mapping := configParams.SelectedMap
if mapping == nil {
@@ -341,22 +524,26 @@ func patchSelectGroup() {
}
}
func applyConfig(rawConfig *config.RawConfig) error {
runLock.Lock()
defer runLock.Unlock()
var err error
currentConfig, err = config.ParseRawConfig(rawConfig)
func applyConfig() error {
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
if err != nil {
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
if configParams.IsPatch {
patchConfig()
patchConfig(cfg.General, cfg.Controller)
} else {
handleCloseConnectionsUnLock()
closeConnections()
runtime.GC()
hub.ApplyConfig(currentConfig)
hub.UltraApplyConfig(cfg)
patchSelectGroup()
}
updateListeners(false)
updateListeners(cfg.General, cfg.Listeners)
if isRunning {
hcCompatibleProvider(cfg.Providers)
}
externalProviders = getExternalProvidersRaw()
return err
}

View File

@@ -1,110 +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"
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"
)

View File

@@ -1,5 +1,3 @@
//go:build cgo
package dart_bridge
/*

View File

@@ -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
View File

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

View File

@@ -1,19 +1,11 @@
module core
go 1.21
go 1.21.0
replace github.com/metacubex/mihomo => ./Clash.Meta
require github.com/metacubex/mihomo v1.17.1
require (
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
)
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
require (
@@ -25,7 +17,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/coreos/go-iptables v0.7.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
@@ -34,6 +26,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -45,7 +38,7 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240812123929-b105c29bd1b5 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
@@ -65,8 +58,8 @@ require (
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd // indirect
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
@@ -86,7 +79,8 @@ require (
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/samber/lo v1.47.0
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
github.com/samber/lo v1.47.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
@@ -103,13 +97,13 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect

View File

@@ -19,8 +19,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -42,6 +42,8 @@ github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXb
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -74,8 +76,8 @@ github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7s
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/insomniacslk/dhcp v0.0.0-20240812123929-b105c29bd1b5 h1:GkMacU5ftc+IEg1449N3UEy2XLDz58W4fkrRu2fibb8=
github.com/insomniacslk/dhcp v0.0.0-20240812123929-b105c29bd1b5/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -94,8 +96,6 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
@@ -120,14 +120,12 @@ github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd h1:r7alry8u4qlUFLNMwGvG1A8ZcfPM6AMSmrm6E2yKdB4=
github.com/metacubex/sing-wireguard v0.0.0-20240826061955-1e4e67afe5cd/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785 h1:NNmI+ZV0DzNuqaAInRQuZFLHlWVuyHeow8jYpdKjHjo=
github.com/metacubex/tfo-go v0.0.0-20240830120620-c5e019b67785/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
@@ -158,8 +156,6 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
@@ -172,6 +168,8 @@ github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnV
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -211,10 +209,6 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -229,18 +223,18 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -260,13 +254,13 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=

View File

@@ -1,71 +1,82 @@
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"runtime"
"sort"
"time"
)
var (
isInit = false
configParams = ConfigExtendedParams{}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
)
var configParams = ConfigExtendedParams{}
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 {
constant.SetHomeDir(homeDirStr)
constant.SetHomeDir(C.GoString(homeDirStr))
isInit = true
}
return isInit
}
func handleStartListener() bool {
runLock.Lock()
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 {
//export getIsInit
func getIsInit() bool {
return isInit
}
func handleForceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
//export restartClash
func restartClash() bool {
execPath, _ := os.Executable()
go restartExecutable(execPath)
return true
}
func handleShutdown() bool {
//export shutdownClash
func shutdownClash() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
@@ -73,81 +84,106 @@ func handleShutdown() bool {
return true
}
func handleValidateConfig(bytes []byte) string {
_, err := config.UnmarshalRawConfig(bytes)
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()
//export forceGc
func forceGc() {
go func() {
defer runLock.Unlock()
var params = &ChangeProxyParams{}
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
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}
func handleGetTraffic(onlyProxy bool) string {
up, down := statistic.DefaultManager.Current(onlyProxy)
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
var updateLock sync.Mutex
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
state.CurrentRawConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export clearEffect
func clearEffect(s *C.char) {
id := C.GoString(s)
go func() {
_ = removeFile(getProfilePath(id))
_ = removeFile(getProfileProvidersPath(id))
}()
}
//export getProxies
func getProxies() *C.char {
data, err := json.Marshal(tunnel.ProxiesWithProviders())
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export changeProxy
func changeProxy(s *C.char) {
paramsString := C.GoString(s)
var params = &ChangeProxyParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
log.Infoln("Unmarshal ChangeProxyParams %v", err)
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
group, ok := proxies[groupName]
if !ok {
return
}
adapterProxy := group.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
return
}
if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName)
}
if err == nil {
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
}
}
//export getTraffic
func getTraffic() *C.char {
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -155,13 +191,14 @@ func handleGetTraffic(onlyProxy bool) string {
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleGetTotalTraffic(onlyProxy bool) string {
up, down := statistic.DefaultManager.Total(onlyProxy)
//export getTotalTraffic
func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -169,27 +206,31 @@ func handleGetTotalTraffic(onlyProxy bool) string {
data, err := json.Marshal(traffic)
if err != nil {
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()
}
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) {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
fn("")
bridge.SendToPort(i, "")
return false, nil
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
fn("")
bridge.SendToPort(i, "")
return false, nil
}
@@ -206,7 +247,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if proxy == nil {
delayData.Value = -1
data, _ := json.Marshal(delayData)
fn(string(data))
bridge.SendToPort(i, string(data))
return false, nil
}
@@ -214,30 +255,44 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if err != nil || delay == 0 {
delayData.Value = -1
data, _ := json.Marshal(delayData)
fn(string(data))
bridge.SendToPort(i, string(data))
return false, nil
}
delayData.Value = int32(delay)
data, _ := json.Marshal(delayData)
fn(string(data))
bridge.SendToPort(i, string(data))
return false, nil
})
}
func handleGetConnections() string {
runLock.Lock()
defer runLock.Unlock()
//export getVersionInfo
func getVersionInfo() *C.char {
versionInfo := map[string]string{
"clashName": constant.Name,
"version": "1.18.5",
}
data, err := json.Marshal(versionInfo)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export getConnections
func getConnections() *C.char {
snapshot := statistic.DefaultManager.Snapshot()
data, err := json.Marshal(snapshot)
if err != nil {
fmt.Println("Error:", err)
return ""
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 {
err := c.Close()
if err != nil {
@@ -245,37 +300,43 @@ func handleCloseConnectionsUnLock() bool {
}
return true
})
return true
}
func handleCloseConnections() bool {
runLock.Lock()
defer runLock.Unlock()
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()
//export closeConnection
func closeConnection(id *C.char) {
connectionId := C.GoString(id)
c := statistic.DefaultManager.Get(connectionId)
if c == nil {
return false
return
}
_ = c.Close()
return true
}
func handleGetExternalProviders() string {
runLock.Lock()
defer runLock.Unlock()
externalProviders = getExternalProvidersRaw()
//export getProviders
func getProviders() *C.char {
data, err := json.Marshal(tunnel.Providers())
var msg *C.char
if err != nil {
msg = C.CString("")
return msg
}
msg = C.CString(string(data))
return msg
}
//export getProvider
func getProvider(name *C.char) *C.char {
providerName := C.GoString(name)
providers := tunnel.Providers()
data, err := json.Marshal(providers[providerName])
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export getExternalProviders
func getExternalProviders() *C.char {
eps := make([]ExternalProvider, 0)
for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p)
@@ -287,125 +348,123 @@ func handleGetExternalProviders() string {
sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps)
if err != nil {
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleGetExternalProvider(externalProviderName string) string {
runLock.Lock()
defer runLock.Unlock()
//export getExternalProvider
func getExternalProvider(name *C.char) *C.char {
externalProviderName := C.GoString(name)
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return ""
return C.CString("")
}
e, err := toExternalProvider(externalProvider)
if err != nil {
return ""
return C.CString("")
}
data, err := json.Marshal(e)
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() {
path := constant.Path.Resolve(geoName)
switch geoType {
path := constant.Path.Resolve(geoNameString)
switch geoTypeString {
case "MMDB":
err := updater.UpdateMMDBWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := updater.UpdateASNWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIpWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSiteWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
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() {
externalProvider, exist := externalProviders[providerName]
externalProvider, exist := externalProviders[providerNameString]
if !exist {
fn("external provider is not exist")
bridge.SendToPort(i, "external provider is not exist")
return
}
err := externalProvider.Update()
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
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() {
runLock.Lock()
defer runLock.Unlock()
externalProvider, exist := externalProviders[providerName]
externalProvider, exist := externalProviders[providerNameString]
if !exist {
fn("external provider is not exist")
bridge.SendToPort(i, "external provider is not exist")
return
}
err := sideUpdateExternalProvider(externalProvider, data)
err := sideUpdateExternalProvider(externalProvider, bytes)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
fn("")
bridge.SendToPort(i, "")
}()
}
func handleStartLog() {
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 initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api)
}
func handleStopLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
//export initMessage
func initMessage(port C.longlong) {
i := int64(port)
Port = i
}
//export freeCString
func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
}
func init() {
adapter.UrlTestHook = func(name string, delay uint16) {
provider.HealthcheckHook = func(name string, delay uint16) {
delayData := &Delay{
Name: name,
}

View File

@@ -1,182 +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 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 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()
}

View File

@@ -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
View 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
}
}

View File

@@ -1,17 +1,10 @@
//go:build !cgo
package main
import "C"
import (
"fmt"
"os"
)
func main() {
args := os.Args
if len(args) <= 1 {
fmt.Println("Arguments error")
os.Exit(1)
}
startServer(args[1])
fmt.Println("init clash")
}

View File

@@ -1,8 +0,0 @@
//go:build cgo
package main
import "C"
func main() {
}

View File

@@ -1,13 +1,77 @@
//go:build !cgo
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) {
s, err := message.Json()
if err != nil {
return
}
Action{
Method: messageMethod,
}.callback(s)
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
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
//go:build android && cgo
//go:build android
package platform

81
core/process.go Normal file
View 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)
}
}()
}

View File

@@ -1,162 +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), &params)
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), &params)
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
}
}

46
core/state.go Normal file
View 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
}
}

View File

@@ -1,11 +1,13 @@
//go:build android && cgo
package state
import "github.com/metacubex/mihomo/config"
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
var CurrentRawConfig = config.DefaultRawConfig()
type AndroidVpnOptions struct {
Enable bool `json:"enable"`
Port int `json:"port"`
@@ -13,7 +15,6 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"`
RouteAddress []string `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"`
@@ -31,7 +32,6 @@ type AndroidVpnRawOptions struct {
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
}
@@ -39,6 +39,7 @@ type AndroidVpnRawOptions struct {
type State struct {
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
OnlyProxy bool `json:"onlyProxy"`
}
var CurrentState = &State{}
@@ -52,5 +53,7 @@ func GetIpv6Address() string {
}
func GetDnsServerAddress() string {
//prefix, _ := netip.ParsePrefix(DefaultIpv4Address)
//return prefix.Addr().String()
return DefaultDnsAddress
}

157
core/tun.go Normal file
View 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
}

View File

@@ -1,11 +1,10 @@
//go:build android && cgo
//go:build android
package tun
import "C"
import (
"core/state"
"github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
@@ -24,7 +23,7 @@ type Props struct {
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
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
if err != nil {
@@ -47,8 +46,8 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
options := LC.Tun{
Enable: true,
Device: device,
Stack: stack,
Device: state.CurrentRawConfig.Tun.Device,
Stack: state.CurrentRawConfig.Tun.Stack,
DNSHijack: dnsHijack,
AutoRoute: false,
AutoDetectInterface: false,

View File

@@ -1,13 +1,9 @@
import 'dart:async';
import 'package:animations/animations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -21,7 +17,6 @@ runAppWithPreferences(
Widget child, {
required AppState appState,
required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
@@ -33,7 +28,7 @@ runAppWithPreferences(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
create: (_) => AppFlowingState(),
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
@@ -63,18 +58,10 @@ class ApplicationState extends State<Application> {
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.windows: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.linux: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.macOS: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
},
);
@@ -106,7 +93,6 @@ class ApplicationState extends State<Application> {
}
await globalState.appController.init();
globalState.appController.initLink();
app?.initShortcuts();
});
}
@@ -177,10 +163,9 @@ class ApplicationState extends State<Application> {
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale,
themeMode: config.themeProps.themeMode,
primaryColor: config.themeProps.primaryColor,
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
prueBlack: config.prueBlack,
),
builder: (_, state, child) {
return DynamicColorBuilder(
@@ -214,7 +199,6 @@ class ApplicationState extends State<Application> {
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
@@ -224,7 +208,6 @@ class ApplicationState extends State<Application> {
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
@@ -247,10 +230,8 @@ class ApplicationState extends State<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
_cancelTimer();
await clashService?.destroy();
await globalState.appController.savePreferences();
await globalState.appController.handleExit();
super.dispose();
_cancelTimer();
}
}

View File

@@ -1,4 +1,3 @@
export 'core.dart';
export 'lib.dart';
export 'message.dart';
export 'service.dart';
export 'message.dart';

View File

@@ -1,26 +1,41 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'generated/clash_ffi.dart';
class ClashCore {
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() {
if (Platform.isAndroid) {
clashInterface = clashLib!;
} else {
clashInterface = clashService!;
}
lib = _getClashLib();
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashCore() {
@@ -28,62 +43,67 @@ class ClashCore {
return _instance!;
}
Future<void> _initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
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);
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
shutdown() {
clashFFI.shutdownClash();
lib.close();
}
bool get isInit => clashFFI.getIsInit() == 1;
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
} 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({
required ClashConfig clashConfig,
required Config config,
}) async {
await _initGeo();
final homeDirPath = await appPath.getHomeDirPath();
return await clashInterface.init(homeDirPath);
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;
}
shutdown() async {
await clashInterface.shutdown();
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
}
FutureOr<bool> get isInit => clashInterface.isInit;
FutureOr<String> validateConfig(String data) {
return clashInterface.validateConfig(data);
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await clashInterface.updateConfig(updateConfigParams);
}
Future<List<Group>> getProxiesGroups() async {
final proxiesRawString = await clashInterface.getProxies();
Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
@@ -113,112 +133,256 @@ class ClashCore {
});
}
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
return await clashInterface.changeProxy(changeProxyParams);
}
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 {
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
await clashInterface.getExternalProviders();
return Isolate.run<List<ExternalProvider>>(
() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
},
);
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
}
Future<ExternalProvider?> getExternalProvider(
String externalProviderName) async {
final externalProvidersRawString =
await clashInterface.getExternalProvider(externalProviderName);
if (externalProvidersRawString == null) {
return null;
}
if (externalProvidersRawString.isEmpty) {
return null;
}
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if (externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
}
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
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({
required String providerName,
required String data,
}) {
return clashInterface.sideLoadExternalProvider(
providerName: providerName, data: data);
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
Future<String> updateExternalProvider({
required String providerName,
}) async {
return clashInterface.updateExternalProvider(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;
}
startListener() async {
await clashInterface.startListener();
changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar);
}
stopListener() async {
await clashInterface.stopListener();
start() {
clashFFI.start();
}
Future<Delay> getDelay(String proxyName) async {
final data = await clashInterface.asyncTestDelay(proxyName);
return Delay.fromJson(json.decode(data));
stop() {
clashFFI.stop();
}
Future<Traffic> getTraffic(bool value) async {
final trafficString = await clashInterface.getTraffic(value);
return Traffic.fromMap(json.decode(trafficString));
Future<Delay> getDelay(String proxyName) {
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;
}
Future<Traffic> getTotalTraffic(bool value) async {
final totalTrafficString = await clashInterface.getTotalTraffic(value);
return Traffic.fromMap(json.decode(totalTrafficString));
clearEffect(String profileId) {
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(profileIdChar);
malloc.free(profileIdChar);
}
resetTraffic() {
clashInterface.resetTraffic();
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(versionInfoRaw);
return VersionInfo.fromJson(versionInfo);
}
startLog() {
clashInterface.startLog();
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() {
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() {
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

View File

@@ -1,59 +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);
resetTraffic();
startLog();
stopLog();
FutureOr<String> getConnections();
FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections();
}

View File

@@ -1,367 +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();
}
/// 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;

View File

@@ -1,40 +1,42 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart';
import 'core.dart';
class ClashMessage {
final controller = StreamController();
StreamSubscription? subscription;
ClashMessage._() {
clashLib?.receiver.listen(controller.add);
controller.stream.listen(
(message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
}
if (subscription != null) {
subscription!.cancel();
subscription = null;
}
subscription = ClashCore.receiver.listen((message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
}
},
);
}
});
}
static final ClashMessage instance = ClashMessage._();

View File

@@ -1,414 +1,54 @@
import 'dart:async';
import 'dart:convert';
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/enum/enum.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
class ClashService with ClashInterface {
static ClashService? _instance;
import 'core.dart';
Completer<ServerSocket> serverCompleter = Completer();
Completer<Socket> socketCompleter = Completer();
Map<String, Completer> callbackCompleterMap = {};
Process? process;
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()),
),
);
},
);
class ClashService {
Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
}
startCore() async {
if (process != null) {
await shutdown();
}
final serverSocket = await serverCompleter.future;
final arg = Platform.isWindows
? "${serverSocket.port}"
: serverSocket.address.address;
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:
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:
default:
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,
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,
),
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
},
);
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 {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
}) async {
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final isInit = clashCore.init(homeDirPath);
return isInit;
}
}
final clashService = system.isDesktop ? ClashService() : null;
final clashService = ClashService();

View File

@@ -29,6 +29,7 @@ extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
background: Colors.black,
surfaceContainer: surfaceContainer.darken(0.05),
)
: this;

View File

@@ -1,36 +1,32 @@
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 'picker.dart';
export 'preferences.dart';
export 'protocol.dart';
export 'proxy.dart';
export 'request.dart';
export 'scroll.dart';
export 'string.dart';
export 'system.dart';
export 'text.dart';
export 'tray.dart';
export 'preferences.dart';
export 'constant.dart';
export 'proxy.dart';
export 'other.dart';
export 'num.dart';
export 'navigation.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 'iterable.dart';
export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';
export 'network.dart';

View File

@@ -1,20 +1,15 @@
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const helperTag = "2024125";
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
@@ -26,7 +21,7 @@ const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop
? !Platform.isMacOS
? 40
: 28
: 26
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":

View File

@@ -24,8 +24,8 @@ class DAVClient {
},
);
client.setConnectTimeout(8000);
client.setSendTimeout(60000);
client.setReceiveTimeout(60000);
client.setSendTimeout(8000);
client.setReceiveTimeout(8000);
pingCompleter.complete(_ping());
}

View File

@@ -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');
}
},
);
}
}

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides {
@override
@@ -11,9 +10,6 @@ class FlClashHttpOverrides extends HttpOverrides {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
debugPrint("find $url");
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart';
import 'system.dart';
import 'windows.dart';
class AutoLaunch {
static AutoLaunch? _instance;
@@ -26,16 +26,60 @@ class AutoLaunch {
return await launchAtStartup.isEnabled();
}
Future<bool> get windowsIsEnable async {
final res = await Process.run(
'schtasks',
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
runInShell: true,
);
return res.stdout.toString().contains(Platform.resolvedExecutable);
}
Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable();
}
windowsDisable() async {
final res = await Process.run(
'schtasks',
[
'/Delete',
'/TN',
appName,
'/F',
],
runInShell: true,
);
return res.exitCode == 0;
}
Future<bool> windowsEnable() async {
await disable();
return await windows?.registerTask(appName) ?? false;
}
Future<bool> disable() async {
return await launchAtStartup.disable();
}
updateStatus(AutoLaunchState state) async {
final isAdminAutoLaunch = state.isAdminAutoLaunch;
final isAutoLaunch = state.isAutoLaunch;
if (Platform.isWindows && isAdminAutoLaunch) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();
if (!isEnable) {
enable();
}
} else {
windowsDisable();
}
return;
}
if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) {
enable();

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
typedef InstallConfigCallBack = void Function(String url);
@@ -17,7 +17,7 @@ class LinkManager {
initAppLinksListen(installConfigCallBack) async {
debugPrint("initAppLinksListen");
destroy();
subscription = _appLinks.uriLinkStream.listen(
subscription = _appLinks.allUriLinkStream.listen(
(uri) {
debugPrint('onAppLink: $uri');
if (uri.host == 'install-config') {
@@ -31,7 +31,8 @@ class LinkManager {
);
}
destroy() {
destroy(){
if (subscription != null) {
subscription?.cancel();
subscription = null;

View File

@@ -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();

View File

@@ -8,8 +8,7 @@ class Measure {
Measure.of(this.context)
: _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
);
WidgetsBinding.instance.platformDispatcher.textScaleFactor);
Size computeTextSize(Text text) {
final textPainter = TextPainter(
@@ -39,6 +38,7 @@ class Measure {
return _bodyMediumHeight!;
}
Size get bodyLargeSize {
_bodyLargeSize ??= computeTextSize(
Text(

View File

@@ -1,11 +0,0 @@
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
return await Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) => child,
),
);
}
}

View File

@@ -2,14 +2,13 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
class Other {
Color? getDelayColor(int? delay) {
@@ -19,14 +18,6 @@ class Other {
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) {
var valueRaw = "0$value";
return valueRaw.substring(
@@ -110,19 +101,17 @@ class Other {
}
String getTrayIconPath({
required bool isStart,
required Brightness brightness,
}) {
if (Platform.isMacOS) {
return "assets/images/icon_white.png";
}
final suffix = Platform.isWindows ? "ico" : "png";
if (Platform.isWindows) {
return "assets/images/icon.$suffix";
if (!isStart && Platform.isWindows) {
return switch (brightness) {
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",
};
return "assets/images/icon.$suffix";
}
int compareVersions(String version1, String version2) {
@@ -196,13 +185,15 @@ class Other {
return parameters[fileNameKey];
}
FlutterView getScreen() {
return WidgetsBinding.instance.platformDispatcher.views.first;
double getViewWidth() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
final size = view.physicalSize / view.devicePixelRatio;
return size.width;
}
List<String> parseReleaseBody(String? body) {
if (body == null) return [];
const pattern = r'- \s*(.*)';
const pattern = r'- (.+?)\. \[.+?\]';
final regex = RegExp(pattern);
return regex
.allMatches(body)

View File

@@ -13,17 +13,34 @@ class AppPath {
Completer<Directory> tempDir = Completer();
late String appDirPath;
// Future<Directory> _createDesktopCacheDir() async {
// final dir = Directory(path);
// if (await dir.exists()) {
// await dir.create(recursive: true);
// }
// return dir;
// }
AppPath._internal() {
appDirPath = join(dirname(Platform.resolvedExecutable));
getApplicationSupportDirectory().then((value) {
dataDir.complete(value);
});
getTemporaryDirectory().then((value) {
tempDir.complete(value);
getTemporaryDirectory().then((value){
tempDir.complete(value);
});
getDownloadsDirectory().then((value) {
downloadDir.complete(value);
});
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
// });
// } else {
// _createDesktopCacheDir().then((value) {
// cacheDir.complete(value);
// });
// }
}
factory AppPath() {
@@ -31,23 +48,6 @@ class AppPath {
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 {
final directory = await downloadDir.future;
return directory.path;
@@ -58,11 +58,6 @@ class AppPath {
return directory.path;
}
Future<String> getLockFilePath() async {
final directory = await dataDir.future;
return join(directory.path, "FlClash.lock");
}
Future<String> getProfilesPath() async {
final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName);
@@ -74,12 +69,6 @@ class AppPath {
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 {
final directory = await tempDir.future;
return directory.path;

View File

@@ -1,5 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
@@ -81,96 +79,25 @@ class Request {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.timeout(httpTimeoutDuration);
if (response.statusCode != 200 || response.data == null) {
continue;
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout(
httpTimeoutDuration,
);
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
}
return source.value(response.data!);
} catch (e) {
if (e is DioException && e.type == DioExceptionType.cancel) {
if (cancelToken?.isCancelled == true) {
throw "cancelled";
}
debugPrint("checkIp error ===> $e");
continue;
}
}
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();

View File

@@ -1,13 +1,11 @@
import 'dart:io';
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/state.dart';
import 'package:fl_clash/widgets/input.dart';
import 'package:flutter/services.dart';
import 'window.dart';
class System {
static System? _instance;
@@ -21,6 +19,12 @@ class System {
bool get isDesktop =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
get isAdmin async {
if (!Platform.isWindows) return false;
final result = await Process.run('net', ['session'], runInShell: true);
return result.exitCode == 0;
}
Future<int> get version async {
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
return switch (Platform.operatingSystem) {
@@ -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 {
await app?.moveTaskToBack();
await window?.hide();

View File

@@ -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.clashConfig.mode = 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();

View File

@@ -1,17 +1,17 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';
import 'protocol.dart';
import 'system.dart';
class Window {
init(WindowProps props, int version) async {
final acquire = await singleInstanceLock.acquire();
if (!acquire) {
exit(0);
}
if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
protocol.register("clash");
protocol.register("clashmeta");
protocol.register("flclash");
@@ -21,7 +21,14 @@ class Window {
size: Size(props.width, props.height),
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.waitUntilReadyToShow(windowOptions, () async {

View File

@@ -1,10 +1,7 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.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';
class Windows {
@@ -54,84 +51,12 @@ class Windows {
calloc.free(argumentsPtr);
calloc.free(operationPtr);
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
if (result < 42) {
if (result <= 32) {
return false;
}
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 {
final taskXml = '''
<?xml version="1.0" encoding="UTF-16"?>

View File

@@ -5,7 +5,6 @@ import 'dart:isolate';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -14,8 +13,9 @@ import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart';
import 'clash/core.dart';
import 'models/models.dart';
import 'common/common.dart';
class AppController {
final BuildContext context;
@@ -27,8 +27,6 @@ class AppController {
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
late Function savePreferencesDebounce;
late Function changeProxyDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -38,19 +36,9 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
savePreferencesDebounce = debounce<Function()>(() async {
await savePreferences();
});
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
changeProxyDebounce = debounce((String groupName, String proxyName) async {
await changeProxy(
groupName: groupName,
proxyName: proxyName,
);
await updateGroups();
});
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
@@ -59,48 +47,35 @@ class AppController {
});
}
restartCore() async {
await globalState.restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
}
updateStatus(bool isStart) async {
if (isStart) {
await globalState.handleStart();
await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
updateRunTime();
updateTraffic();
globalState.updateFunctionLists = [
updateRunTime,
updateTraffic,
];
final currentLastModified =
await config.getCurrentProfile()?.profileLastModified;
if (currentLastModified == null ||
globalState.lastProfileModified == null) {
addCheckIpNumDebounce();
return;
if (!Platform.isAndroid) {
applyProfileDebounce();
}
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
addCheckIpNumDebounce();
return;
}
applyProfileDebounce();
} else {
await globalState.handleStop();
await clashCore.resetTraffic();
clashCore.resetTraffic();
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
await Future.delayed(
Duration(milliseconds: 300),
);
addCheckIpNumDebounce();
}
}
updateCoreVersionInfo() {
globalState.updateCoreVersionInfo(appState);
}
updateRunTime() {
final startTime = globalState.startTime;
if (startTime != null) {
@@ -114,7 +89,6 @@ class AppController {
updateTraffic() {
globalState.updateTraffic(
config: config,
appFlowingState: appFlowingState,
);
}
@@ -127,7 +101,7 @@ class AppController {
deleteProfile(String id) async {
config.deleteProfileById(id);
clearEffect(id);
clashCore.clearEffect(id);
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
@@ -139,10 +113,6 @@ class AppController {
}
}
updateProviders() {
globalState.updateProviders(appState);
}
Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update();
config.setProfile(
@@ -155,7 +125,6 @@ class AppController {
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
@@ -231,16 +200,25 @@ class AppController {
}
savePreferences() async {
debugPrint("[APP] savePreferences");
await saveConfigPreferences();
await saveClashConfigPreferences();
}
saveConfigPreferences() async {
debugPrint("saveConfigPreferences");
await preferences.saveConfig(config);
}
saveClashConfigPreferences() async {
debugPrint("saveClashConfigPreferences");
await preferences.saveClashConfig(clashConfig);
}
changeProxy({
required String groupName,
required String proxyName,
}) async {
await globalState.changeProxy(
}) {
globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
@@ -251,7 +229,7 @@ class AppController {
handleBackOrExit() async {
if (config.appSetting.minimizeOnExit) {
if (system.isDesktop) {
await savePreferencesDebounce();
await savePreferences();
}
await system.back();
} else {
@@ -260,16 +238,22 @@ class AppController {
}
handleExit() async {
try {
await updateStatus(false);
await clashCore.shutdown();
await clashService?.destroy();
await proxy?.stopProxy();
await savePreferences();
} catch (_) {}
await updateStatus(false);
await proxy?.stopProxy();
await savePreferences();
clashCore.shutdown();
system.exit();
}
updateLogStatus() {
if (config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
appFlowingState.logs = [];
}
}
autoCheckUpdate() async {
if (!config.appSetting.autoCheckUpdate) return;
final res = await request.checkForUpdate();
@@ -324,20 +308,10 @@ class AppController {
if (!isDisclaimerAccepted) {
handleExit();
}
updateLogStatus();
if (!config.appSetting.silentLaunch) {
window?.show();
}
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
await _initStatus();
autoUpdateProfiles();
autoCheckUpdate();
}
_initStatus() async {
if (Platform.isAndroid) {
globalState.updateStartTime();
}
@@ -346,6 +320,8 @@ class AppController {
} else {
await updateStatus(config.appSetting.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
}
setDelay(Delay delay) {
@@ -553,19 +529,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() {
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
@@ -573,8 +536,8 @@ class AppController {
}
updateSystemProxy() {
config.networkProps = config.networkProps.copyWith(
systemProxy: !config.networkProps.systemProxy,
config.desktopProps = config.desktopProps.copyWith(
systemProxy: !config.desktopProps.systemProxy,
);
}
@@ -588,6 +551,12 @@ class AppController {
);
}
updateAdminAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: !config.appSetting.adminAutoLaunch,
);
}
updateVisible() async {
final visible = await window?.isVisible();
if (visible != null && !visible) {
@@ -636,16 +605,6 @@ class AppController {
});
}
updateTray([bool focus = false]) async {
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
focus: focus,
);
}
recoveryData(
List<int> data,
RecoveryOption recoveryOption,

View File

@@ -162,55 +162,3 @@ enum ProxiesIconStyle {
none,
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,
}
enum AuthorizeCode { none, success, error }
enum WindowsHelperServiceStatus {
none,
presence,
running,
}

View File

@@ -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 {
const ApplicationSettingFragment({super.key});
@@ -108,6 +134,8 @@ class ApplicationSettingFragment extends StatelessWidget {
);
},
),
if(Platform.isWindows)
const AdminAutoLaunchItem(),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,

View File

@@ -24,7 +24,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
title: appLocalizations.network,
isScaffold: true,
isBlur: false,
extendPageWidth: 360,
widget: const NetworkListView(),
),
),

View File

@@ -6,11 +6,10 @@ import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class VPNItem extends StatelessWidget {
const VPNItem({super.key});
class VPNSwitch extends StatelessWidget {
const VPNSwitch({super.key});
@override
Widget build(BuildContext context) {
@@ -40,8 +39,8 @@ class TUNItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<ClashConfig, bool>(
selector: (_, clashConfig) => clashConfig.tun.enable,
return Selector<Config, bool>(
selector: (_, config) => config.vpnProps.enable,
builder: (_, enable, __) {
return ListItem.switchItem(
title: Text(appLocalizations.tun),
@@ -61,8 +60,8 @@ class TUNItem extends StatelessWidget {
}
}
class AllowBypassItem extends StatelessWidget {
const AllowBypassItem({super.key});
class AllowBypassSwitch extends StatelessWidget {
const AllowBypassSwitch({super.key});
@override
Widget build(BuildContext context) {
@@ -88,8 +87,8 @@ class AllowBypassItem extends StatelessWidget {
}
}
class VpnSystemProxyItem extends StatelessWidget {
const VpnSystemProxyItem({super.key});
class SystemProxySwitch extends StatelessWidget {
const SystemProxySwitch({super.key});
@override
Widget build(BuildContext context) {
@@ -115,35 +114,8 @@ class VpnSystemProxyItem extends StatelessWidget {
}
}
class SystemProxyItem extends StatelessWidget {
const SystemProxyItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
builder: (_, systemProxy, __) {
return ListItem.switchItem(
title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate(
value: systemProxy,
onChanged: (bool value) async {
final config = globalState.appController.config;
final networkProps = config.networkProps;
config.networkProps = networkProps.copyWith(
systemProxy: value,
);
},
),
);
},
);
}
}
class Ipv6Item extends StatelessWidget {
const Ipv6Item({super.key});
class Ipv6Switch extends StatelessWidget {
const Ipv6Switch({super.key});
@override
Widget build(BuildContext context) {
@@ -204,36 +176,6 @@ class TunStackItem extends StatelessWidget {
class BypassDomainItem extends StatelessWidget {
const BypassDomainItem({super.key});
_initActions(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
globalState.showMessage(
title: appLocalizations.reset,
message: TextSpan(
text: appLocalizations.resetTip,
),
onTab: () {
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
bypassDomain: defaultBypassDomain,
);
Navigator.of(context).pop();
},
);
},
tooltip: appLocalizations.reset,
icon: const Icon(
Icons.replay,
),
)
];
});
}
@override
Widget build(BuildContext context) {
return ListItem.open(
@@ -241,20 +183,19 @@ class BypassDomainItem extends StatelessWidget {
subtitle: Text(appLocalizations.bypassDomainDesc),
delegate: OpenDelegate(
isBlur: false,
isScaffold: true,
title: appLocalizations.bypassDomain,
widget: Selector<Config, List<String>>(
selector: (_, config) => config.networkProps.bypassDomain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (context, bypassDomain, __) {
_initActions(context);
selector: (_, config) => config.vpnProps.bypassDomain,
shouldRebuild: (prev, next) =>
!stringListEquality.equals(prev, next),
builder: (_, bypassDomain, __) {
return ListPage(
title: appLocalizations.bypassDomain,
items: bypassDomain,
titleBuilder: (item) => Text(item),
onChange: (items) {
onChange: (items){
final config = globalState.appController.config;
config.networkProps = config.networkProps.copyWith(
config.vpnProps = config.vpnProps.copyWith(
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 = [
if (Platform.isAndroid) const VPNItem(),
Platform.isAndroid ? const VPNSwitch() : const TUNItem(),
if (Platform.isAndroid)
...generateSection(
title: "VPN",
items: [
const SystemProxyItem(),
const AllowBypassItem(),
const Ipv6Item(),
],
),
if (system.isDesktop)
...generateSection(
title: appLocalizations.system,
items: [
SystemProxyItem(),
BypassDomainItem(),
const SystemProxySwitch(),
const AllowBypassSwitch(),
const Ipv6Switch(),
const BypassDomainItem(),
],
),
...generateSection(
title: appLocalizations.options,
items: [
if (system.isDesktop) const TUNItem(),
const TunStackItem(),
const RouteModeItem(),
const RouteAddressItem(),
],
),
];

View File

@@ -27,19 +27,18 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) async {
(timer) {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
connections: clashCore.getConnections(),
);
},
);
@@ -67,10 +66,10 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
width: 8,
),
IconButton(
onPressed: () async {
onPressed: () {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
connections: clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
@@ -100,11 +99,10 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
);
}
_handleBlockConnection(String id) async {
_handleBlockConnection(String id) {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
}
@override
@@ -241,10 +239,10 @@ class ConnectionsSearchDelegate extends SearchDelegate {
);
}
_handleBlockConnection(String id) async {
_handleBlockConnection(String id) {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
connections: clashCore.getConnections(),
);
}

View File

@@ -2,16 +2,15 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/fragments/dashboard/status_button.dart';
import 'package:fl_clash/fragments/dashboard/status_switch.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:provider/provider.dart';
import 'network_detection.dart';
import 'network_speed.dart';
import 'outbound_mode.dart';
import 'start_button.dart';
import 'network_speed.dart';
import 'traffic_usage.dart';
class DashboardFragment extends StatefulWidget {
@@ -22,25 +21,12 @@ class DashboardFragment extends StatefulWidget {
}
class _DashboardFragmentState extends State<DashboardFragment> {
_initFab(bool isCurrent) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = const StartButton();
});
}
@override
Widget build(BuildContext context) {
return ActiveBuilder(
label: "dashboard",
builder: (isCurrent, child) {
_initFab(isCurrent);
return child!;
},
return FloatLayout(
floatingWidget: const FloatWrapper(
child: StartButton(),
),
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
@@ -69,11 +55,11 @@ class _DashboardFragmentState extends State<DashboardFragment> {
if (system.isDesktop) ...[
GridItem(
crossAxisCellCount: switchCount,
child: const TUNButton(),
child: const TUNSwitch(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const SystemProxyButton(),
child: const ProxySwitch(),
),
],
const GridItem(

View File

@@ -1,7 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -16,14 +14,14 @@ class IntranetIP extends StatefulWidget {
class _IntranetIPState extends State<IntranetIP> {
final ipNotifier = ValueNotifier<String?>("");
late StreamSubscription subscription;
Future<String> getNetworkType() async {
try {
final interfaces = await NetworkInterface.list(
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')) {
@@ -35,6 +33,7 @@ class _IntranetIPState extends State<IntranetIP> {
return 'Mobile Data';
}
}
return 'Unknown';
} catch (e) {
return 'Error';
@@ -42,7 +41,6 @@ class _IntranetIPState extends State<IntranetIP> {
}
Future<String?> getLocalIpAddress() async {
await Future.delayed(animateDuration);
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,
)
@@ -68,14 +66,15 @@ class _IntranetIPState extends State<IntranetIP> {
return null;
}
@override
void dispose() {
super.dispose();
ipNotifier.dispose();
}
@override
void initState() {
super.initState();
subscription = Connectivity().onConnectivityChanged.listen((_) async {
ipNotifier.value = null;
debugPrint("[App] Connection change");
ipNotifier.value = await getLocalIpAddress() ?? "";
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
ipNotifier.value = await getLocalIpAddress() ?? "";
});
@@ -105,9 +104,7 @@ class _IntranetIPState extends State<IntranetIP> {
flex: 1,
child: TooltipText(
text: Text(
value.isNotEmpty
? value
: appLocalizations.noNetwork,
value.isNotEmpty ? value : appLocalizations.noNetwork,
style: context
.textTheme.titleLarge?.toSoftBold.toMinus,
maxLines: 1,
@@ -130,11 +127,4 @@ class _IntranetIPState extends State<IntranetIP> {
),
);
}
@override
void dispose() {
super.dispose();
subscription.cancel();
ipNotifier.dispose();
}
}

View File

@@ -2,20 +2,12 @@ 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});
@@ -24,6 +16,12 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State<NetworkDetection> {
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
bool? _preIsStart;
Function? _checkIpDebounce;
Timer? _setTimeoutTimer;
@@ -56,25 +54,22 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
return;
}
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (e) {
if (e.toString() == "cancelled") {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
} catch (_) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
}
_clearSetTimeoutTimer() {
if (_setTimeoutTimer != null) {
if(_setTimeoutTimer != null){
_setTimeoutTimer?.cancel();
_setTimeoutTimer = null;
}
@@ -96,8 +91,9 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
@override
dispose() {
void dispose() {
super.dispose();
networkDetectionState.dispose();
}
String countryCodeToEmoji(String countryCode) {
@@ -159,8 +155,7 @@ class _NetworkDetectionState extends State<NetworkDetection> {
.textTheme
.titleLarge
?.copyWith(
fontFamily:
FontFamily.twEmoji.value,
fontFamily: "Twemoji",
),
),
)

View File

@@ -19,10 +19,9 @@ class _StartButtonState extends State<StartButton>
@override
void initState() {
super.initState();
isStart = globalState.appController.appFlowingState.isStart;
_controller = AnimationController(
vsync: this,
value: isStart ? 1 : 0,
value: 0,
duration: const Duration(milliseconds: 200),
);
}
@@ -86,58 +85,58 @@ class _StartButtonState extends State<StartButton>
)
.width +
16;
return _updateControllerContainer(
AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
),
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56 + textWidth * _controller.value,
height: 56,
child: FloatingActionButton(
heroTag: null,
onPressed: () {
handleSwitchStart();
},
child: Row(
children: [
Container(
width: 56,
height: 56,
alignment: Alignment.center,
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _controller,
),
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
),
),
Expanded(
child: ClipRect(
child: OverflowBox(
maxWidth: textWidth,
child: Container(
alignment: Alignment.centerLeft,
child: child!,
),
),
),
],
),
),
],
),
);
},
child: child,
),
),
);
},
child: child,
);
},
child: Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
);
},
child: _updateControllerContainer(
Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
);
},
),
),
);
}

View File

@@ -1,33 +1,45 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/system.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../config/network.dart';
// class 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 TUNButton extends StatelessWidget {
const TUNButton({super.key});
class TUNSwitch extends StatelessWidget {
const TUNSwitch({super.key});
@override
Widget build(BuildContext context) {
return ButtonContainer(
onPressed: () {
showSheet(
context: context,
builder: (_) {
return generateListView(generateSection(
items: [
if (system.isDesktop) const TUNItem(),
const TunStackItem(),
],
));
},
title: appLocalizations.tun,
);
},
return SwitchContainer(
info: Info(
label: appLocalizations.tun,
iconData: Icons.stacked_line_chart,
@@ -52,34 +64,18 @@ class TUNButton extends StatelessWidget {
}
}
class SystemProxyButton extends StatelessWidget {
const SystemProxyButton({super.key});
class ProxySwitch extends StatelessWidget {
const ProxySwitch({super.key});
@override
Widget build(BuildContext context) {
return ButtonContainer(
onPressed: () {
showSheet(
context: context,
builder: (_) {
return generateListView(
generateSection(
items: [
SystemProxyItem(),
BypassDomainItem(),
],
),
);
},
title: appLocalizations.systemProxy,
);
},
return SwitchContainer(
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Selector<Config, bool>(
selector: (_, config) => config.networkProps.systemProxy,
selector: (_, config) => config.desktopProps.systemProxy,
builder: (_, systemProxy, __) {
return LocaleBuilder(
builder: (_) => Switch(
@@ -87,8 +83,8 @@ class SystemProxyButton extends StatelessWidget {
value: systemProxy,
onChanged: (value) {
final config = globalState.appController.config;
config.networkProps =
config.networkProps.copyWith(systemProxy: value);
config.desktopProps =
config.desktopProps.copyWith(systemProxy: value);
},
),
);
@@ -98,22 +94,20 @@ class SystemProxyButton extends StatelessWidget {
}
}
class ButtonContainer extends StatelessWidget {
class SwitchContainer extends StatelessWidget {
final Info info;
final Widget child;
final VoidCallback onPressed;
const ButtonContainer({
const SwitchContainer({
super.key,
required this.info,
required this.child,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return CommonCard(
onPressed: onPressed,
onPressed: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -7,10 +7,7 @@ import 'package:flutter/material.dart';
class AddProfile extends StatelessWidget {
final BuildContext context;
const AddProfile({
super.key,
required this.context,
});
const AddProfile({super.key, required this.context,});
_handleAddProfileFormFile() async {
globalState.appController.addProfileFormFile();
@@ -21,16 +18,14 @@ class AddProfile extends StatelessWidget {
}
_toScan() async {
if (system.isDesktop) {
if(system.isDesktop){
globalState.appController.addProfileFormQrCode();
return;
}
final url = await BaseNavigator.push(
context,
const ScanPage(),
);
final url = await Navigator.of(context)
.push<String>(MaterialPageRoute(builder: (_) => const ScanPage()));
if (url != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_){
_handleAddProfileFormURL(url);
});
}
@@ -49,12 +44,12 @@ class AddProfile extends StatelessWidget {
Widget build(context) {
return ListView(
children: [
ListItem(
leading: const Icon(Icons.qr_code),
title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc),
onTap: _toScan,
),
ListItem(
leading: const Icon(Icons.qr_code),
title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc),
onTap: _toScan,
),
ListItem(
leading: const Icon(Icons.upload_file),
title: Text(appLocalizations.file),

View File

@@ -1,9 +1,9 @@
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
@@ -80,7 +80,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
_initScaffold() {
_initScaffoldState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
@@ -112,67 +112,71 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
iconSize: 26,
),
];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
);
},
);
}
@override
Widget build(BuildContext context) {
return ActiveBuilder(
label: "profiles",
builder: (isCurrent, child) {
if (isCurrent) {
_initScaffold();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
return FloatLayout(
floatingWidget: FloatWrapper(
child: FloatingActionButton(
heroTag: null,
onPressed: _handleShowAddExtendPage,
child: const Icon(
Icons.add,
),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
),
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
);
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
);
},
),
),
);
}
@@ -246,16 +250,44 @@ class ProfileItem extends StatelessWidget {
);
}
List<Widget> _buildUserInfo(BuildContext context, UserInfo userInfo) {
final use = userInfo.upload + userInfo.download;
final total = userInfo.total;
if (total == 0) {
return [];
}
final useShow = TrafficValue(value: use).show;
final totalShow = TrafficValue(value: total).show;
final progress = total == 0 ? 0.0 : use / total;
final expireShow = userInfo.expire == 0
? appLocalizations.infiniteTime
: DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show;
return [
LinearProgressIndicator(
minHeight: 6,
value: progress,
backgroundColor: context.colorScheme.primary.toSoft(),
),
const SizedBox(
height: 8,
),
Text(
"$useShow / $totalShow · $expireShow",
style: context.textTheme.labelMedium?.toLight,
),
const SizedBox(
height: 4,
),
];
}
List<Widget> _buildUrlProfileInfo(BuildContext context) {
final subscriptionInfo = profile.subscriptionInfo;
final userInfo = profile.userInfo;
return [
const SizedBox(
height: 8,
),
if (subscriptionInfo != null)
SubscriptionInfoView(
subscriptionInfo: subscriptionInfo,
),
if (userInfo != null) ..._buildUserInfo(context, userInfo),
Text(
profile.lastUpdateDate?.lastUpdateTimeDesc ?? "",
style: context.textTheme.labelMedium?.toLight,

View File

@@ -115,10 +115,11 @@ class ProxyCard extends StatelessWidget {
groupName,
nextProxyName,
);
await appController.changeProxyDebounce([
groupName,
nextProxyName,
]);
appController.changeProxy(
groupName: groupName,
proxyName: nextProxyName,
);
await appController.updateGroupDebounce();
return;
}
globalState.showSnackBar(

View File

@@ -51,7 +51,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
}
_headerStateNotifier.value = _headerStateNotifier.value.copyWith(
currentIndex: currentIndex,
offset: max(headerOffset, 0),
offset: headerOffset,
);
}
@@ -299,9 +299,6 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
headerState.currentIndex > state.groupNames.length - 1
? 0
: headerState.currentIndex;
if (index < 0) {
return Container();
}
return Stack(
children: [
Positioned(
@@ -420,9 +417,9 @@ class _ListHeaderState extends State<ListHeader>
final iconMapEntryList =
config.proxiesStyle.iconMap.entries.toList();
final index = iconMapEntryList.indexWhere((item) {
try {
try{
return RegExp(item.key).hasMatch(groupName);
} catch (_) {
}catch(_){
return false;
}
});
@@ -471,7 +468,7 @@ class _ListHeaderState extends State<ListHeader>
Widget build(BuildContext context) {
return CommonCard(
key: widget.key,
radius: 18,
radius: 24,
type: CommonCardType.filled,
child: Container(
padding: const EdgeInsets.symmetric(

View File

@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
@@ -27,7 +27,6 @@ class _ProvidersState extends State<Providers> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) {
globalState.appController.updateProviders();
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
@@ -56,7 +55,7 @@ class _ProvidersState extends State<Providers> {
providerName: provider.name,
);
appState.setProvider(
await clashCore.getExternalProvider(provider.name),
clashCore.getExternalProvider(provider.name),
);
},
);
@@ -122,7 +121,7 @@ class ProviderItem extends StatelessWidget {
if (message.isNotEmpty) throw message;
});
appState.setProvider(
await clashCore.getExternalProvider(provider.name),
clashCore.getExternalProvider(provider.name),
);
});
await globalState.appController.updateGroupDebounce();
@@ -133,8 +132,8 @@ class ProviderItem extends StatelessWidget {
final platformFile = await picker.pickerFile();
final appState = globalState.appController.appState;
final bytes = platformFile?.bytes;
if (bytes == null || provider.path == null) return;
final file = await File(provider.path!).create(recursive: true);
if (bytes == null) return;
final file = await File(provider.path).create(recursive: true);
await file.writeAsBytes(bytes);
final providerName = provider.name;
var message = await clashCore.sideLoadExternalProvider(
@@ -143,7 +142,7 @@ class ProviderItem extends StatelessWidget {
);
if (message.isNotEmpty) throw message;
appState.setProvider(
await clashCore.getExternalProvider(provider.name),
clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
});
@@ -151,7 +150,8 @@ class ProviderItem extends StatelessWidget {
}
String _buildProviderDesc() {
final baseInfo = provider.updateAt.lastUpdateTimeDesc;
final baseInfo =
"${provider.type}(${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}";
final count = provider.count;
return switch (count == 0) {
true => baseInfo,
@@ -176,13 +176,10 @@ class ProviderItem extends StatelessWidget {
Text(
_buildProviderDesc(),
),
const SizedBox(
height: 4,
Text(
provider.path,
style: context.textTheme.bodyMedium?.toLight,
),
if (provider.subscriptionInfo != null)
SubscriptionInfoView(
subscriptionInfo: provider.subscriptionInfo,
),
const SizedBox(
height: 8,
),
@@ -203,9 +200,6 @@ class ProviderItem extends StatelessWidget {
),
],
),
const SizedBox(
height: 4,
),
],
),
trailing: SizedBox(

View File

@@ -191,7 +191,7 @@ class ProxiesSetting extends StatelessWidget {
_buildGroupStyleSetting() {
return generateSection(
title: appLocalizations.iconStyle,
title: "图标样式",
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),

View File

@@ -1,5 +1,4 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -11,8 +10,6 @@ import 'package:provider/provider.dart';
import 'card.dart';
import 'common.dart';
List<Proxy> currentProxies = [];
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
class ProxiesTabFragment extends StatefulWidget {
@@ -31,7 +28,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
@override
void dispose() {
super.dispose();
_destroyTabController();
_tabController?.dispose();
}
scrollToGroupSelected() {
@@ -109,36 +106,6 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
);
}
_tabControllerListener([int? index]) {
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (_tabController?.index == null) {
return;
}
final currentGroup = currentGroups[index ?? _tabController!.index];
currentProxies = currentGroup.all;
appController.config.updateCurrentGroupName(
currentGroup.name,
);
}
_destroyTabController() {
_tabController?.removeListener(_tabControllerListener);
_tabController?.dispose();
_tabController = null;
}
_updateTabController(int length, int index) {
final realIndex = index == -1 ? 0 : index;
_tabController ??= TabController(
length: length,
initialIndex: realIndex,
vsync: this,
);
_tabControllerListener(realIndex);
_tabController?.addListener(_tabControllerListener);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
@@ -152,7 +119,8 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
},
shouldRebuild: (prev, next) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_destroyTabController();
_tabController?.dispose();
_tabController = null;
return true;
}
return false;
@@ -161,8 +129,12 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_updateTabController(state.groupNames.length, index);
final GroupNameKeyMap keyMap = {};
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
);
GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
return KeepScope(
@@ -195,6 +167,16 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
left: 16,
right: 16 + (value ? 16 : 0),
),
onTap: (index) {
final appController = globalState.appController;
final currentGroups =
appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(
currentGroups[index].name,
);
}
},
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
@@ -261,13 +243,14 @@ class ProxyGroupView extends StatefulWidget {
class ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final _controller = ScrollController();
List<Proxy> _lastProxies = [];
String get groupName => widget.groupName;
_delayTest() async {
_delayTest(List<Proxy> proxies) async {
if (isLock) return;
isLock = true;
await delayTest(currentProxies);
await delayTest(proxies);
isLock = false;
}
@@ -286,7 +269,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: currentProxies,
proxies: _lastProxies,
),
_controller.position.maxScrollExtent,
),
@@ -295,21 +278,6 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
);
}
initFab(bool isCurrent) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest();
},
);
});
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
@@ -334,11 +302,12 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
return ActiveBuilder(
label: "proxies",
builder: (isCurrent, child) {
initFab(isCurrent);
return child!;
_lastProxies = sortedProxies;
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
child: Align(
alignment: Alignment.topCenter,
@@ -375,19 +344,22 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
}
}
class DelayTestButton extends StatefulWidget {
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButton({
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButton> createState() => _DelayTestButtonState();
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonState extends State<DelayTestButton>
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
@@ -395,9 +367,7 @@ class _DelayTestButtonState extends State<DelayTestButton>
_healthcheck() async {
_controller.forward();
await widget.onClick();
if (mounted) {
_controller.reverse();
}
_controller.reverse();
}
@override
@@ -431,23 +401,29 @@ class _DelayTestButtonState extends State<DelayTestButton>
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
),
child: widget.child,
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
@@ -19,16 +18,6 @@ class ThemeModeItem {
});
}
class FontFamilyItem {
final FontFamily fontFamily;
final String label;
const FontFamilyItem({
required this.fontFamily,
required this.label,
});
}
class ThemeFragment extends StatelessWidget {
const ThemeFragment({super.key});
@@ -103,11 +92,7 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
return CommonCard(
isSelected: isSelected,
onPressed: () {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
themeMode: themeModeItem.themeMode,
);
globalState.appController.config.themeMode = themeModeItem.themeMode;
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -140,45 +125,11 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
isSelected: isSelected,
primaryColor: color,
onPressed: () {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
primaryColor: color?.value,
);
globalState.appController.config.primaryColor = color?.value;
},
);
}
Widget _fontFamilyCheckBox({
bool? isSelected,
required FontFamilyItem fontFamilyItem,
}) {
return CommonCard(
isSelected: isSelected,
onPressed: () {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
fontFamily: fontFamilyItem.fontFamily,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Text(
fontFamilyItem.label,
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
List<ThemeModeItem> themeModeItems = [
@@ -207,59 +158,15 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Colors.yellowAccent,
Colors.purple,
];
List<FontFamilyItem> fontFamilyItems = [
FontFamilyItem(
label: appLocalizations.systemFont,
fontFamily: FontFamily.system,
),
const FontFamilyItem(
label: "MiSans",
fontFamily: FontFamily.miSans,
),
];
return Column(
children: [
ItemCard(
info: Info(
label: appLocalizations.fontFamily,
iconData: Icons.text_fields,
),
child: Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
height: 48,
child: Selector<Config, FontFamily>(
selector: (_, config) => config.themeProps.fontFamily,
builder: (_, fontFamily, __) {
return ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) {
final fontFamilyItem = fontFamilyItems[index];
return _fontFamilyCheckBox(
isSelected: fontFamily == fontFamilyItem.fontFamily,
fontFamilyItem: fontFamilyItem,
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
itemCount: fontFamilyItems.length,
);
},
),
),
),
ItemCard(
info: Info(
label: appLocalizations.themeMode,
iconData: Icons.brightness_high,
),
child: Selector<Config, ThemeMode>(
selector: (_, config) => config.themeProps.themeMode,
selector: (_, config) => config.themeMode,
builder: (_, themeMode, __) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -297,7 +204,7 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
),
height: 88,
child: Selector<Config, int?>(
selector: (_, config) => config.themeProps.primaryColor,
selector: (_, config) => config.primaryColor,
builder: (_, currentPrimaryColor, __) {
return ListView.separated(
scrollDirection: Axis.horizontal,
@@ -322,7 +229,7 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Selector<Config, bool>(
selector: (_, config) => config.themeProps.prueBlack,
selector: (_, config) => config.prueBlack,
builder: (_, value, ___) {
return ListItem.switchItem(
leading: Icon(
@@ -331,19 +238,63 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
),
title: Text(appLocalizations.prueBlackMode),
delegate: SwitchDelegate(
value: value,
onChanged: (value) {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
prueBlack: value,
);
},
),
value: value,
onChanged: (value) {
globalState.appController.config.prueBlack = value;
}),
);
},
),
),
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 16),
// child: Selector<Config, bool>(
// selector: (_, config) => config.scaleProps.custom,
// builder: (_, value, ___) {
// return ListItem.switchItem(
// leading: Icon(
// Icons.format_size_sharp,
// color: context.colorScheme.primary,
// ),
// title: const Text("自定义字体大小"),
// delegate: SwitchDelegate(
// value: value,
// onChanged: (value) {
// globalState.appController.config.scaleProps =
// globalState.appController.config.scaleProps.copyWith(
// custom: value,
// );
// },
// ),
// );
// },
// ),
// ),
// SizedBox(
// height: 20,
// child: Selector<Config, ScaleProps>(
// selector: (_, config) => config.scaleProps,
// builder: (_, props, ___) {
// return AbsorbPointer(
// absorbing: !props.custom,
// child: DisabledMask(
// status: !props.custom,
// child: Slider(
// value: props.scale,
// min: 0.8,
// max: 1.2,
// onChanged: (value) {
// globalState.appController.config.scaleProps =
// globalState.appController.config.scaleProps.copyWith(
// scale: value,
// );
// },
// ),
// ),
// );
// },
// ),
// ),
const SizedBox(
height: 64,
),

View File

@@ -168,7 +168,6 @@
"ipv6Desc": "When turned on it will be able to receive IPv6 traffic",
"app": "App",
"general": "General",
"vpnSystemProxyDesc": "Attach HTTP proxy to VpnService",
"systemProxyDesc": "Attach HTTP proxy to VpnService",
"unifiedDelay": "Unified delay",
"unifiedDelayDesc": "Remove extra delays such as handshaking",
@@ -321,16 +320,5 @@
"iconConfiguration": "Icon configuration",
"noData": "No data",
"adminAutoLaunch": "Admin auto launch",
"adminAutoLaunchDesc": "Boot up by using admin mode",
"fontFamily": "FontFamily",
"systemFont": "System font",
"toggle": "Toggle",
"system": "System",
"routeMode": "Route mode",
"routeMode_bypassPrivate": "Bypass private route address",
"routeMode_config": "Use config",
"routeAddress": "Route address",
"routeAddressDesc": "Config listen route address",
"pleaseInputAdminPassword": "Please enter the admin password",
"copyEnvVar": "Copying environment variables"
"adminAutoLaunchDesc": "Boot up by using admin mode"
}

View File

@@ -168,8 +168,7 @@
"ipv6Desc": "开启后将可以接收IPv6流量",
"app": "应用",
"general": "基础",
"vpnSystemProxyDesc": "为VpnService附加HTTP代理",
"systemProxyDesc": "设置系统代理",
"systemProxyDesc": "为VpnService附加HTTP代理",
"unifiedDelay": "统一延迟",
"unifiedDelayDesc": "去除握手等额外延迟",
"tcpConcurrent": "TCP并发",
@@ -321,16 +320,5 @@
"iconConfiguration": "图片配置",
"noData": "暂无数据",
"adminAutoLaunch": "管理员自启动",
"adminAutoLaunchDesc": "使用管理员模式开机自启动",
"fontFamily": "字体",
"systemFont": "系统字体",
"toggle": "切换",
"system": "系统",
"routeMode": "路由模式",
"routeMode_bypassPrivate": "绕过私有路由地址",
"routeMode_config": "使用配置",
"routeAddress": "路由地址",
"routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量"
}
"adminAutoLaunchDesc": "使用管理员模式开机自启动"
}

View File

@@ -121,8 +121,6 @@ class MessageLookup extends MessageLookupByLibrary {
"View current connections data"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage(
"Copying environment variables"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
@@ -194,7 +192,6 @@ class MessageLookup extends MessageLookupByLibrary {
"findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage(
"There is a risk of flashback after opening"),
"fontFamily": MessageLookupByLibrary.simpleMessage("FontFamily"),
"fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"),
"general": MessageLookupByLibrary.simpleMessage("General"),
"generalDesc":
@@ -328,8 +325,6 @@ class MessageLookup extends MessageLookupByLibrary {
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
"pleaseBindWebDAV":
MessageLookupByLibrary.simpleMessage("Please bind WebDAV"),
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"Please enter the admin password"),
"pleaseUploadFile":
MessageLookupByLibrary.simpleMessage("Please upload file"),
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
@@ -402,13 +397,6 @@ class MessageLookup extends MessageLookupByLibrary {
"respectRules": MessageLookupByLibrary.simpleMessage("Respect rules"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS connection following rules, need to configure proxy-server-nameserver"),
"routeAddress": MessageLookupByLibrary.simpleMessage("Route address"),
"routeAddressDesc":
MessageLookupByLibrary.simpleMessage("Config listen route address"),
"routeMode": MessageLookupByLibrary.simpleMessage("Route mode"),
"routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage(
"Bypass private route address"),
"routeMode_config": MessageLookupByLibrary.simpleMessage("Use config"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
@@ -437,8 +425,6 @@ class MessageLookup extends MessageLookupByLibrary {
"style": MessageLookupByLibrary.simpleMessage("Style"),
"submit": MessageLookupByLibrary.simpleMessage("Submit"),
"sync": MessageLookupByLibrary.simpleMessage("Sync"),
"system": MessageLookupByLibrary.simpleMessage("System"),
"systemFont": MessageLookupByLibrary.simpleMessage("System font"),
"systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
@@ -459,7 +445,6 @@ class MessageLookup extends MessageLookupByLibrary {
"tight": MessageLookupByLibrary.simpleMessage("Tight"),
"time": MessageLookupByLibrary.simpleMessage("Time"),
"tip": MessageLookupByLibrary.simpleMessage("tip"),
"toggle": MessageLookupByLibrary.simpleMessage("Toggle"),
"tools": MessageLookupByLibrary.simpleMessage("Tools"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"),
"tun": MessageLookupByLibrary.simpleMessage("TUN"),
@@ -487,8 +472,6 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Modify VPN related settings"),
"vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
"Auto routes all system traffic through VpnService"),
"vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Attach HTTP proxy to VpnService"),
"vpnTip": MessageLookupByLibrary.simpleMessage(
"Changes take effect after restarting the VPN"),
"webDAVConfiguration":

View File

@@ -98,7 +98,6 @@ class MessageLookup extends MessageLookupByLibrary {
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"),
@@ -156,7 +155,6 @@ class MessageLookup extends MessageLookupByLibrary {
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"findProcessModeDesc":
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"general": MessageLookupByLibrary.simpleMessage("基础"),
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
@@ -260,8 +258,6 @@ class MessageLookup extends MessageLookupByLibrary {
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
"pleaseInputAdminPassword":
MessageLookupByLibrary.simpleMessage("请输入管理员密码"),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseUploadValidQrcode":
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
@@ -317,12 +313,6 @@ class MessageLookup extends MessageLookupByLibrary {
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS连接跟随rules,需配置proxy-server-nameserver"),
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
"routeMode_bypassPrivate":
MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
@@ -349,10 +339,9 @@ class MessageLookup extends MessageLookupByLibrary {
"style": MessageLookupByLibrary.simpleMessage("风格"),
"submit": MessageLookupByLibrary.simpleMessage("提交"),
"sync": MessageLookupByLibrary.simpleMessage("同步"),
"system": MessageLookupByLibrary.simpleMessage("系统"),
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
"systemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tabAnimationDesc":
@@ -368,7 +357,6 @@ class MessageLookup extends MessageLookupByLibrary {
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
"time": MessageLookupByLibrary.simpleMessage("时间"),
"tip": MessageLookupByLibrary.simpleMessage("提示"),
"toggle": MessageLookupByLibrary.simpleMessage("切换"),
"tools": MessageLookupByLibrary.simpleMessage("工具"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
@@ -390,8 +378,6 @@ class MessageLookup extends MessageLookupByLibrary {
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
"vpnEnableDesc":
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"),
"vpnSystemProxyDesc":
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),

View File

@@ -1740,16 +1740,6 @@ class AppLocalizations {
);
}
/// `Attach HTTP proxy to VpnService`
String get vpnSystemProxyDesc {
return Intl.message(
'Attach HTTP proxy to VpnService',
name: 'vpnSystemProxyDesc',
desc: '',
args: [],
);
}
/// `Attach HTTP proxy to VpnService`
String get systemProxyDesc {
return Intl.message(
@@ -3279,116 +3269,6 @@ class AppLocalizations {
args: [],
);
}
/// `FontFamily`
String get fontFamily {
return Intl.message(
'FontFamily',
name: 'fontFamily',
desc: '',
args: [],
);
}
/// `System font`
String get systemFont {
return Intl.message(
'System font',
name: 'systemFont',
desc: '',
args: [],
);
}
/// `Toggle`
String get toggle {
return Intl.message(
'Toggle',
name: 'toggle',
desc: '',
args: [],
);
}
/// `System`
String get system {
return Intl.message(
'System',
name: 'system',
desc: '',
args: [],
);
}
/// `Route mode`
String get routeMode {
return Intl.message(
'Route mode',
name: 'routeMode',
desc: '',
args: [],
);
}
/// `Bypass private route address`
String get routeMode_bypassPrivate {
return Intl.message(
'Bypass private route address',
name: 'routeMode_bypassPrivate',
desc: '',
args: [],
);
}
/// `Use config`
String get routeMode_config {
return Intl.message(
'Use config',
name: 'routeMode_config',
desc: '',
args: [],
);
}
/// `Route address`
String get routeAddress {
return Intl.message(
'Route address',
name: 'routeAddress',
desc: '',
args: [],
);
}
/// `Config listen route address`
String get routeAddressDesc {
return Intl.message(
'Config listen route address',
name: 'routeAddressDesc',
desc: '',
args: [],
);
}
/// `Please enter the admin password`
String get pleaseInputAdminPassword {
return Intl.message(
'Please enter the admin password',
name: 'pleaseInputAdminPassword',
desc: '',
args: [],
);
}
/// `Copying environment variables`
String get copyEnvVar {
return Intl.message(
'Copying environment variables',
name: 'copyEnvVar',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -8,22 +8,17 @@ import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'application.dart';
import 'common/common.dart';
import 'l10n/l10n.dart';
import 'models/models.dart';
import 'common/common.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
clashLib?.initMessage();
clashCore.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform();
final version = await system.version;
final config = await preferences.getConfig() ?? Config();
await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await android?.init();
await window?.init(config.windowProps, version);
@@ -32,14 +27,12 @@ Future<void> main() async {
version: version,
selectedMap: config.currentSelectedMap,
);
final appFlowingState = AppFlowingState();
appState.navigationItems = navigation.getItems(
openLogs: config.appSetting.openLogs,
hasProxies: false,
);
tray.update(
await globalState.init(
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
);
@@ -47,7 +40,6 @@ Future<void> main() async {
runAppWithPreferences(
const Application(),
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
);
@@ -61,70 +53,39 @@ Future<void> vpnService() async {
final version = await system.version;
final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
final appState = AppState(
mode: clashConfig.mode,
selectedMap: config.currentSelectedMap,
version: version,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await app?.tip(appLocalizations.startVpn);
globalState
.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: false,
)
.then(
(_) async {
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic(config: config);
globalState.updateFunctionLists = [
() {
globalState.updateTraffic(config: config);
}
];
},
);
vpn?.setServiceMessageHandler(
ServiceMessageHandler(
onProtect: (Fd fd) async {
await vpn?.setProtect(fd.value);
clashLib?.setFdMap(fd.id);
clashCore.setFdMap(fd.id);
},
onProcess: (ProcessData process) async {
onProcess: (Process process) async {
final packageName = await vpn?.resolverProcess(process);
clashLib?.setProcessMap(
clashCore.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
},
onStarted: (String runTime) async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
},
onLoaded: (String groupName) {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
@@ -137,20 +98,50 @@ Future<void> vpnService() async {
},
),
);
final appLocalizations = await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
await app?.tip(appLocalizations.startVpn);
await globalState.handleStart(
config: config,
clashConfig: clashConfig,
);
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic();
globalState.updateFunctionLists = [
() {
globalState.updateTraffic();
}
];
}
@immutable
class ServiceMessageHandler with ServiceMessageListener {
final Function(Fd fd) _onProtect;
final Function(ProcessData process) _onProcess;
final Function(Process process) _onProcess;
final Function(String runTime) _onStarted;
final Function(String providerName) _onLoaded;
const ServiceMessageHandler({
required Function(Fd fd) onProtect,
required Function(ProcessData process) onProcess,
required Function(Process process) onProcess,
required Function(String runTime) onStarted,
required Function(String providerName) onLoaded,
}) : _onProtect = onProtect,
_onProcess = onProcess,
_onStarted = onStarted,
_onLoaded = onLoaded;
@override
@@ -159,10 +150,15 @@ class ServiceMessageHandler with ServiceMessageListener {
}
@override
onProcess(ProcessData process) {
onProcess(Process process) {
_onProcess(process);
}
@override
onStarted(String runTime) {
_onStarted(runTime);
}
@override
onLoaded(String providerName) {
_onLoaded(providerName);

View File

@@ -1,4 +1,3 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/material.dart';
@@ -24,28 +23,6 @@ class _AndroidContainerState extends State<AndroidManager> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.networkProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
routeAddress: clashConfig.routeAddress,
),
builder: (__, state, child) {
clashLib?.setState(state);
return child!;
},
child: child,
);
}
Widget _excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
@@ -59,10 +36,6 @@ class _AndroidContainerState extends State<AndroidManager> {
@override
Widget build(BuildContext context) {
return _updateCoreState(
_excludeContainer(
widget.child,
),
);
return _excludeContainer(widget.child);
}
}

View File

@@ -18,6 +18,7 @@ class AppStateManager extends StatefulWidget {
class _AppStateManagerState extends State<AppStateManager>
with WidgetsBindingObserver {
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
@@ -44,22 +45,6 @@ class _AppStateManagerState extends State<AppStateManager>
);
}
_cacheStateChange(Widget child) {
return Selector2<Config, ClashConfig, String>(
selector: (_, config, clashConfig) => "$clashConfig $config",
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.savePreferencesDebounce();
}
return prev != next;
},
builder: (context, state, child) {
return child!;
},
child: child,
);
}
@override
void initState() {
super.initState();
@@ -76,7 +61,7 @@ class _AppStateManagerState extends State<AppStateManager>
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
final isPaused = state == AppLifecycleState.paused;
if (isPaused) {
globalState.appController.savePreferencesDebounce();
await globalState.appController.savePreferences();
}
}
@@ -88,10 +73,8 @@ class _AppStateManagerState extends State<AppStateManager>
@override
Widget build(BuildContext context) {
return _cacheStateChange(
_updateNavigationsContainer(
widget.child,
),
return _updateNavigationsContainer(
widget.child,
);
}
}

View File

@@ -20,6 +20,7 @@ class ClashManager extends StatefulWidget {
}
class _ClashContainerState extends State<ClashManager> with AppMessageListener {
Function? updateClashConfigDebounce;
Function? updateDelayDebounce;
Widget _updateContainer(Widget child) {
@@ -46,7 +47,10 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
),
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.updateClashConfigDebounce();
updateClashConfigDebounce ??= debounce<Function()>(() async {
await globalState.appController.updateClashConfig();
});
updateClashConfigDebounce!();
}
return prev != next;
},
@@ -57,20 +61,40 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
);
}
Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.vpnProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
),
builder: (__, state, child) {
clashCore.setState(state);
return child!;
},
child: child,
);
}
_changeProfile() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final appController = globalState.appController;
appController.appState.delayMap = {};
await appController.applyProfile();
});
}
Widget _changeProfileContainer(Widget child) {
return Selector<Config, String?>(
selector: (_, config) => config.currentProfileId,
shouldRebuild: (prev, next) {
if (prev != next) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final appController = globalState.appController;
appController.appState.delayMap = {};
appController.applyProfile();
});
}
return prev != next;
},
builder: (__, state, child) {
_changeProfile();
return child!;
},
child: child,
@@ -80,8 +104,10 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
@override
Widget build(BuildContext context) {
return _changeProfileContainer(
_updateContainer(
widget.child,
_updateCoreState(
_updateContainer(
widget.child,
),
),
);
}
@@ -116,6 +142,7 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
if (log.logLevel == LogLevel.error) {
globalState.appController.showSnackBar(log.payload ?? '');
}
// debugPrint("$log");
super.onLog(log);
}
@@ -132,14 +159,14 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
}
@override
Future<void> onLoaded(String providerName) async {
void onLoaded(String providerName) {
final appController = globalState.appController;
appController.appState.setProvider(
await clashCore.getExternalProvider(
clashCore.getExternalProvider(
providerName,
),
);
await appController.updateGroupDebounce();
// appController.addCheckIpNumDebounce();
super.onLoaded(providerName);
}
}

View File

@@ -8,13 +8,13 @@ class ProxyManager extends StatelessWidget {
const ProxyManager({super.key, required this.child});
_updateProxy(ProxyState proxyState) async {
_updateProxy(ProxyState proxyState) {
final isStart = proxyState.isStart;
final systemProxy = proxyState.systemProxy;
final port = proxyState.port;
if (isStart && systemProxy) {
proxy?.startProxy(port, proxyState.bassDomain);
} else {
proxy?.startProxy(port);
}else{
proxy?.stopProxy();
}
}
@@ -24,9 +24,8 @@ class ProxyManager extends StatelessWidget {
return Selector3<AppFlowingState, Config, ClashConfig, ProxyState>(
selector: (_, appFlowingState, config, clashConfig) => ProxyState(
isStart: appFlowingState.isStart,
systemProxy: config.networkProps.systemProxy,
systemProxy: config.desktopProps.systemProxy,
port: clashConfig.mixedPort,
bassDomain: config.networkProps.bypassDomain,
),
builder: (_, state, child) {
_updateProxy(state);

View File

@@ -15,6 +15,8 @@ class TileManager extends StatefulWidget {
}
class _TileContainerState extends State<TileManager> with TileListener {
@override
Widget build(BuildContext context) {
return widget.child;
@@ -27,7 +29,7 @@ class _TileContainerState extends State<TileManager> with TileListener {
}
@override
Future<void> onStop() async {
void onStop() {
globalState.appController.updateStatus(false);
super.onStop();
}

View File

@@ -1,7 +1,11 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart';
@@ -24,29 +28,141 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
trayManager.addListener(this);
}
_updateSystemTray({
required bool isStart,
required Brightness? brightness,
}) async {
if (Platform.isLinux) {
await trayManager.destroy();
}
await trayManager.setIcon(
other.getTrayIconPath(
isStart: isStart,
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
);
if (!Platform.isLinux) {
await trayManager.setToolTip(
appName,
);
}
}
_updateTray(TrayState trayState) async {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!Platform.isLinux) {
_updateSystemTray(
isStart: trayState.isStart,
brightness: trayState.brightness,
);
}
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
onClick: (_) {
window?.show();
},
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label:
trayState.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 == trayState.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (trayState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: trayState.tunEnable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: trayState.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: trayState.autoLaunch,
);
final adminAutoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.adminAutoLaunch,
onClick: (_) async {
globalState.appController.updateAdminAutoLaunch();
},
checked: trayState.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();
menu.items = menuItems;
trayManager.setContextMenu(menu);
if (Platform.isLinux) {
_updateSystemTray(
isStart: trayState.isStart,
brightness: trayState.brightness,
);
}
});
}
@override
Widget build(BuildContext context) {
return Selector4<AppState, AppFlowingState, Config, ClashConfig, TrayState>(
selector: (_, appState, appFlowingState, config, clashConfig) =>
TrayState(
mode: clashConfig.mode,
adminAutoLaunch: config.appSetting.adminAutoLaunch,
autoLaunch: config.appSetting.autoLaunch,
isStart: appFlowingState.isStart,
locale: config.appSetting.locale,
systemProxy: config.networkProps.systemProxy,
systemProxy: config.desktopProps.systemProxy,
tunEnable: clashConfig.tun.enable,
brightness: appState.brightness,
port: clashConfig.mixedPort,
groups: appState.groups,
map: appState.selectedMap,
),
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.updateTray();
}
return prev != next;
},
builder: (_, state, child) {
_updateTray(state);
return child!;
},
child: widget.child,

View File

@@ -5,7 +5,6 @@ import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:window_ext/window_ext.dart';
import 'package:window_manager/window_manager.dart';
class WindowManager extends StatefulWidget {
@@ -20,14 +19,14 @@ class WindowManager extends StatefulWidget {
State<WindowManager> createState() => _WindowContainerState();
}
class _WindowContainerState extends State<WindowManager>
with WindowListener, WindowExtListener {
class _WindowContainerState extends State<WindowManager> with WindowListener {
Function? updateLaunchDebounce;
_autoLaunchContainer(Widget child) {
return Selector<Config, AutoLaunchState>(
selector: (_, config) => AutoLaunchState(
isAutoLaunch: config.appSetting.autoLaunch,
isAdminAutoLaunch: config.appSetting.adminAutoLaunch,
),
builder: (_, state, child) {
updateLaunchDebounce ??= debounce((AutoLaunchState state) {
@@ -48,7 +47,6 @@ class _WindowContainerState extends State<WindowManager>
@override
void initState() {
super.initState();
windowExtManager.addListener(this);
windowManager.addListener(this);
}
@@ -58,15 +56,15 @@ class _WindowContainerState extends State<WindowManager>
super.onWindowClose();
}
@override
Future<void> onShouldTerminate() async {
await globalState.appController.handleExit();
super.onShouldTerminate();
}
@override
Future<void> onWindowMoved() async {
super.onWindowMoved();
final offset = await windowManager.getPosition();
final config = globalState.appController.config;
config.windowProps = config.windowProps.copyWith(
top: offset.dy,
left: offset.dx,
);
}
@override
@@ -82,21 +80,13 @@ class _WindowContainerState extends State<WindowManager>
@override
void onWindowMinimize() async {
globalState.appController.savePreferencesDebounce();
await globalState.appController.savePreferences();
super.onWindowMinimize();
}
@override
Future<void> onTaskbarCreated() async {
globalState.appController.updateTray(true);
await globalState.appController.restartCore();
super.onTaskbarCreated();
}
@override
Future<void> dispose() async {
windowManager.removeListener(this);
windowExtManager.removeListener(this);
super.dispose();
}
}

View File

@@ -1,9 +1,8 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'common.dart';
import 'core.dart';
import 'ffi.dart';
import 'profile.dart';
typedef DelayMap = Map<String, int?>;

View File

@@ -7,9 +7,10 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import '../enum/enum.dart';
part 'generated/clash_config.freezed.dart';
part 'generated/clash_config.g.dart';
part 'generated/clash_config.freezed.dart';
const defaultTun = Tun();
@freezed
@@ -125,91 +126,6 @@ typedef HostsMap = Map<String, String>;
const defaultMixedPort = 7890;
const defaultKeepAliveInterval = 30;
const defaultBypassPrivateRouteAddress = [
"1.0.0.0/8",
"2.0.0.0/7",
"4.0.0.0/6",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/3",
"96.0.0.0/4",
"112.0.0.0/5",
"120.0.0.0/6",
"124.0.0.0/7",
"126.0.0.0/8",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/8",
"169.0.0.0/9",
"169.128.0.0/10",
"169.192.0.0/11",
"169.224.0.0/12",
"169.240.0.0/13",
"169.248.0.0/14",
"169.252.0.0/15",
"169.255.0.0/16",
"170.0.0.0/7",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
"240.0.0.0/5",
"248.0.0.0/6",
"252.0.0.0/7",
"254.0.0.0/8",
"255.0.0.0/9",
"255.128.0.0/10",
"255.192.0.0/11",
"255.224.0.0/12",
"255.240.0.0/13",
"255.248.0.0/14",
"255.252.0.0/15",
"255.254.0.0/16",
"255.255.0.0/17",
"255.255.128.0/18",
"255.255.192.0/19",
"255.255.224.0/20",
"255.255.240.0/21",
"255.255.248.0/22",
"255.255.252.0/23",
"255.255.254.0/24",
"255.255.255.0/25",
"255.255.255.128/26",
"255.255.255.192/27",
"255.255.255.224/28",
"255.255.255.240/29",
"255.255.255.248/30",
"255.255.255.252/31",
"255.255.255.254/32",
"::/1",
"8000::/2",
"c000::/3",
"e000::/4",
"f000::/5",
"f800::/6",
"fe00::/9",
"fec0::/10"
];
@JsonSerializable()
class ClashConfig extends ChangeNotifier {
int _mixedPort;
@@ -229,8 +145,6 @@ class ClashConfig extends ChangeNotifier {
List<String> _rules;
String? _globalRealUa;
HostsMap _hosts;
List<String> _includeRouteAddress;
RouteMode _routeMode;
ClashConfig()
: _mixedPort = defaultMixedPort,
@@ -247,8 +161,6 @@ class ClashConfig extends ChangeNotifier {
_keepAliveInterval = defaultKeepAliveInterval,
_dns = defaultDns,
_geoXUrl = defaultGeoXMap,
_routeMode = RouteMode.config,
_includeRouteAddress = [],
_rules = [],
_hosts = {};
@@ -431,34 +343,6 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "route-address", includeFromJson: false, includeToJson: true)
List<String> get routeAddress {
return switch (_routeMode == RouteMode.config) {
true => _includeRouteAddress,
false => defaultBypassPrivateRouteAddress,
};
}
@JsonKey(name: "include-route-address", defaultValue: [])
List<String> get includeRouteAddress => _includeRouteAddress;
set includeRouteAddress(List<String> value) {
if (!stringListEquality.equals(value, _includeRouteAddress)) {
_includeRouteAddress = value;
notifyListeners();
}
}
@JsonKey(name: "route-mode", defaultValue: RouteMode.config)
RouteMode get routeMode => _routeMode;
set routeMode(RouteMode value) {
if (value != _routeMode) {
_routeMode = value;
notifyListeners();
}
}
update([ClashConfig? clashConfig]) {
if (clashConfig != null) {
_mixedPort = clashConfig._mixedPort;
@@ -476,34 +360,10 @@ class ClashConfig extends ChangeNotifier {
_geodataLoader = clashConfig._geodataLoader;
_dns = clashConfig._dns;
_rules = clashConfig._rules;
_routeMode = clashConfig._routeMode;
_includeRouteAddress = clashConfig._includeRouteAddress;
}
notifyListeners();
}
ClashConfig copyWith() {
return ClashConfig()
..mixedPort = _mixedPort
..mode = _mode
..ipv6 = _ipv6
..findProcessMode = _findProcessMode
..allowLan = _allowLan
..tcpConcurrent = _tcpConcurrent
..logLevel = _logLevel
..tun = tun
..unifiedDelay = _unifiedDelay
..geodataLoader = _geodataLoader
..externalController = _externalController
..keepAliveInterval = _keepAliveInterval
..dns = _dns
..geoXUrl = _geoXUrl
..routeMode = _routeMode
..includeRouteAddress = _includeRouteAddress
..rules = _rules
..hosts = _hosts;
}
Map<String, dynamic> toJson() {
return _$ClashConfigToJson(this);
}
@@ -511,9 +371,4 @@ class ClashConfig extends ChangeNotifier {
factory ClashConfig.fromJson(Map<String, dynamic> json) {
return _$ClashConfigFromJson(json);
}
@override
String toString() {
return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _ipv6: $_ipv6, _geodataLoader: $_geodataLoader, _logLevel: $_logLevel, _externalController: $_externalController, _mode: $_mode, _findProcessMode: $_findProcessMode, _keepAliveInterval: $_keepAliveInterval, _unifiedDelay: $_unifiedDelay, _tcpConcurrent: $_tcpConcurrent, _tun: $_tun, _dns: $_dns, _geoXUrl: $_geoXUrl, _rules: $_rules, _globalRealUa: $_globalRealUa, _hosts: $_hosts}';
}
}

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