Compare commits
12 Commits
v0.8.82-pr
...
v0.8.87-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060e1b5392 | ||
|
|
1477f9bd9c | ||
|
|
a06e813249 | ||
|
|
afbc5adb05 | ||
|
|
76c9f08d4a | ||
|
|
f83a8e0cce | ||
|
|
f5544f1af7 | ||
|
|
eeb543780a | ||
|
|
676f2d058a | ||
|
|
fb9d0cb22c | ||
|
|
e7eb312254 | ||
|
|
c9cd80bcb3 |
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: 问题反馈 / Bug report
|
||||
title: "[BUG] "
|
||||
description: 反馈你遇到的问题 / Report the issue you are experiencing
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
1. 请务必给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请务必按照模板规范详细描述问题,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述 / Describe the bug
|
||||
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 软件版本 / Version
|
||||
description: 请提供FlClash的具体版本 / Please provide the specific version of FlClash.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤 / To Reproduce
|
||||
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统 / OS
|
||||
options:
|
||||
- Android
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: 操作系统版本 / OS Version
|
||||
description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug / Please provide a complete or relevant Debug log (please adjust it to debug in the left menu of software-> Settings-> Log Level)
|
||||
validations:
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: 讨论交流 / Communication
|
||||
url: https://t.me/+G-veVtwBOl4wODc1
|
||||
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: 功能请求 / Feature request
|
||||
title: "[Feature] "
|
||||
description: 提出你的功能请求 / Propose your feature request
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
1. 请务必给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请务必按照模板规范详细描述问题,否则issue将会被直接关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 功能描述 / Feature description
|
||||
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 使用场景 / Use case
|
||||
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: os-labels
|
||||
attributes:
|
||||
label: 适用系统 / Target OS
|
||||
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
|
||||
options:
|
||||
- label: Android
|
||||
- label: Windows
|
||||
- label: MacOS
|
||||
- label: Linux
|
||||
validations:
|
||||
required: true
|
||||
56
.github/workflows/build.yaml
vendored
56
.github/workflows/build.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
os: windows-latest
|
||||
arch: amd64
|
||||
- platform: linux
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-22.04
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-13
|
||||
@@ -27,29 +27,27 @@ jobs:
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
arch: arm64
|
||||
- platform: windows
|
||||
os: windows-11-arm
|
||||
arch: arm64
|
||||
- platform: linux
|
||||
os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Setup rust
|
||||
if: startsWith(matrix.os, 'windows-11-arm')
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-toolchain stable
|
||||
$cargoPath = "$env:USERPROFILE\.cargo\bin"
|
||||
Add-Content $env:GITHUB_PATH $cargoPath
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JAVA
|
||||
if: startsWith(matrix.platform,'android')
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup NDK
|
||||
if: startsWith(matrix.platform,'android')
|
||||
uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r28
|
||||
add-to-path: true
|
||||
link-to-sdk: true
|
||||
|
||||
- name: Setup Android Signing
|
||||
if: startsWith(matrix.platform,'android')
|
||||
run: |
|
||||
@@ -58,19 +56,26 @@ jobs:
|
||||
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
|
||||
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
|
||||
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.24.0'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
|
||||
- name: Setup Flutter
|
||||
- name: Setup Flutter Master
|
||||
if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
channel: 'master'
|
||||
cache: true
|
||||
- name: Setup Flutter
|
||||
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
# flutter-version: 3.29.3
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
@@ -204,6 +209,7 @@ jobs:
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
@@ -214,6 +220,14 @@ jobs:
|
||||
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
|
||||
|
||||
- name: Generate sha256
|
||||
if: env.IS_STABLE == 'true'
|
||||
run: |
|
||||
cd ./dist
|
||||
for file in $(find . -type f -not -name "*.sha256"); do
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
done
|
||||
|
||||
- name: Release
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,3 +1,73 @@
|
||||
## v0.8.85
|
||||
|
||||
- Support override script
|
||||
|
||||
- Support proxies search
|
||||
|
||||
- Support svg display
|
||||
|
||||
- Optimize config persistence
|
||||
|
||||
- Add some scenes auto close connections
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
## v0.8.84
|
||||
|
||||
- Fix windows service verify issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.83
|
||||
|
||||
- Add windows server mode start process verify
|
||||
|
||||
- Add linux deb dependencies
|
||||
|
||||
- Add backup recovery strategy select
|
||||
|
||||
- Support custom text scaling
|
||||
|
||||
- Optimize the display of different text scale
|
||||
|
||||
- Optimize windows setup experience
|
||||
|
||||
- Optimize startTun performance
|
||||
|
||||
- Optimize android tv experience
|
||||
|
||||
- Optimize default option
|
||||
|
||||
- Optimize computed text size
|
||||
|
||||
- Optimize hyperOS freeform window
|
||||
|
||||
- Add developer mode
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Add issues template
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.82
|
||||
|
||||
- Optimize android vpn performance
|
||||
|
||||
- Add custom primary color and color scheme
|
||||
|
||||
- Add linux nad windows arm release
|
||||
|
||||
- Optimize requests and logs page
|
||||
|
||||
- Fix map input page delete issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.81
|
||||
|
||||
- Add rule override
|
||||
|
||||
2
Makefile
2
Makefile
@@ -2,6 +2,8 @@ android_arm64:
|
||||
dart ./setup.dart android --arch arm64
|
||||
macos_arm64:
|
||||
dart ./setup.dart macos --arch arm64
|
||||
android_app:
|
||||
dart ./setup.dart android
|
||||
android_arm64_core:
|
||||
dart ./setup.dart android --arch arm64 --out core
|
||||
macos_arm64_core:
|
||||
|
||||
@@ -41,8 +41,8 @@ on Mobile:
|
||||
⚠️ Make sure to install the following dependencies before using them
|
||||
|
||||
```bash
|
||||
sudo apt-get install appindicator3-0.1 libappindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install libkeybinder-3.0-dev
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
@@ -41,8 +41,8 @@ on Mobile:
|
||||
⚠️ 使用前请确保安装以下依赖
|
||||
|
||||
```bash
|
||||
sudo apt-get install appindicator3-0.1 libappindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install libkeybinder-3.0-dev
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/l10n/intl/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_single_quotes: true
|
||||
@@ -1,96 +0,0 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
def defStoreFile = file("keystore.jks")
|
||||
def defStorePassword = localProperties.getProperty('storePassword')
|
||||
def defKeyAlias = localProperties.getProperty('keyAlias')
|
||||
def defKeyPassword = localProperties.getProperty('keyPassword')
|
||||
def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias != null && defKeyPassword != null
|
||||
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 35
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isRelease) {
|
||||
release {
|
||||
storeFile defStoreFile
|
||||
storePassword defStorePassword
|
||||
keyAlias defKeyAlias
|
||||
keyPassword defKeyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.follow.clash"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
release {
|
||||
if (isRelease) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
|
||||
exclude group: "com.google.guava", module: "guava"
|
||||
}
|
||||
}
|
||||
93
android/app/build.gradle.kts
Normal file
93
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,93 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val localPropertiesFile = rootProject.file("local.properties")
|
||||
val localProperties = Properties().apply {
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val mStoreFile: File = file("keystore.jks")
|
||||
val mStorePassword: String? = localProperties.getProperty("storePassword")
|
||||
val mKeyAlias: String? = localProperties.getProperty("keyAlias")
|
||||
val mKeyPassword: String? = localProperties.getProperty("keyPassword")
|
||||
val isRelease = mStoreFile.exists()
|
||||
&& mStorePassword != null
|
||||
&& mKeyAlias != null
|
||||
&& mKeyPassword != null
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash"
|
||||
compileSdk = 35
|
||||
ndkVersion = "28.0.13004108"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.follow.clash"
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isRelease) {
|
||||
create("release") {
|
||||
storeFile = mStoreFile
|
||||
storePassword = mStorePassword
|
||||
keyAlias = mKeyAlias
|
||||
keyPassword = mKeyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isDebuggable = false
|
||||
|
||||
signingConfig = if (isRelease) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
|
||||
exclude(group = "com.google.guava", module = "guava")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -22,6 +25,7 @@
|
||||
|
||||
<application
|
||||
android:name=".FlClashApplication"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash">
|
||||
@@ -44,6 +48,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
@@ -87,7 +92,7 @@
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:icon="@drawable/ic"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="n">
|
||||
|
||||
@@ -7,6 +7,10 @@ import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
@@ -20,6 +24,10 @@ enum class RunState {
|
||||
object GlobalState {
|
||||
val runLock = ReentrantLock()
|
||||
|
||||
const val NOTIFICATION_CHANNEL = "FlClash"
|
||||
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
private var serviceEngine: FlutterEngine? = null
|
||||
@@ -29,6 +37,15 @@ object GlobalState {
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun syncStatus() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val status = getCurrentVPNPlugin()?.getStatus() ?: false
|
||||
withContext(Dispatchers.Main){
|
||||
runState.value = if (status) RunState.START else RunState.STOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getText(text: String): String {
|
||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
@@ -18,6 +17,7 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
GlobalState.flutterEngine = null
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
@@ -105,7 +104,7 @@ fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||
fun InetAddress.asSocketAddressText(port: Int): String {
|
||||
return when (this) {
|
||||
is Inet6Address ->
|
||||
"[${numericToTextFormat(this.address)}]:$port"
|
||||
"[${numericToTextFormat(this)}]:$port"
|
||||
|
||||
is Inet4Address ->
|
||||
"${this.hostAddress}:$port"
|
||||
@@ -142,7 +141,8 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
|
||||
}
|
||||
}
|
||||
|
||||
private fun numericToTextFormat(src: ByteArray): String {
|
||||
private fun numericToTextFormat(address: Inet6Address): String {
|
||||
val src = address.address
|
||||
val sb = StringBuilder(39)
|
||||
for (i in 0 until 8) {
|
||||
sb.append(
|
||||
@@ -155,6 +155,10 @@ private fun numericToTextFormat(src: ByteArray): String {
|
||||
sb.append(":")
|
||||
}
|
||||
}
|
||||
if (address.scopeId > 0) {
|
||||
sb.append("%")
|
||||
sb.append(address.scopeId)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.follow.clash.models
|
||||
data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val isSystem: Boolean,
|
||||
val system: Boolean,
|
||||
val internet: Boolean,
|
||||
val lastUpdateTime: Long,
|
||||
)
|
||||
|
||||
@@ -293,19 +293,17 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
|
||||
?.filter {
|
||||
it.packageName != FlClashApplication.getAppContext().packageName && (
|
||||
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
)
|
||||
it.packageName != FlClashApplication.getAppContext().packageName || it.packageName == "android"
|
||||
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
lastUpdateTime = it.lastUpdateTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
lastUpdateTime = it.lastUpdateTime,
|
||||
internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
fun handleStart(options: VpnOptions): Boolean {
|
||||
onUpdateNetwork();
|
||||
if (options.enable != this.options?.enable) {
|
||||
this.flClashService = null
|
||||
}
|
||||
@@ -168,8 +169,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
try {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
|
||||
val startForegroundParams = Gson().fromJson(
|
||||
val startForegroundParams = if (data != null) Gson().fromJson(
|
||||
data, StartForegroundParams::class.java
|
||||
) else StartForegroundParams(
|
||||
title = "", content = ""
|
||||
)
|
||||
if (lastStartForegroundParams != startForegroundParams) {
|
||||
lastStartForegroundParams = startForegroundParams
|
||||
@@ -198,6 +201,13 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
timerJob = null
|
||||
}
|
||||
|
||||
|
||||
suspend fun getStatus(): Boolean? {
|
||||
return withContext(Dispatchers.Default) {
|
||||
flutterMethodChannel.awaitResult<Boolean>("status", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartService() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
@@ -246,9 +256,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashService?.stop()
|
||||
stopForegroundJob()
|
||||
Core.stopTun()
|
||||
flClashService?.stop()
|
||||
GlobalState.handleTryDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
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.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
interface BaseServiceInterface {
|
||||
|
||||
@@ -9,4 +28,69 @@ interface BaseServiceInterface {
|
||||
fun stop()
|
||||
|
||||
suspend fun startForeground(title: String, content: String)
|
||||
}
|
||||
|
||||
fun Service.createFlClashNotificationBuilder(): Deferred<NotificationCompat.Builder> =
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@createFlClashNotificationBuilder, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@createFlClashNotificationBuilder,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@createFlClashNotificationBuilder, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(
|
||||
NotificationCompat.Builder(
|
||||
this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL
|
||||
)
|
||||
) {
|
||||
setSmallIcon(R.drawable.ic)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0, stopText, getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
fun Service.startForeground(notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
|
||||
if (channel == null) {
|
||||
channel = NotificationChannel(
|
||||
GlobalState.NOTIFICATION_CHANNEL, "SERVICE_CHANNEL", NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
try {
|
||||
startForeground(
|
||||
GlobalState.NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
startForeground(GlobalState.NOTIFICATION_ID, notification)
|
||||
}
|
||||
} else {
|
||||
startForeground(GlobalState.NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,51 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
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.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
private suspend fun notificationBuilder(): NotificationCompat.Builder {
|
||||
if (cachedBuilder == null) {
|
||||
cachedBuilder = createFlClashNotificationBuilder().await()
|
||||
}
|
||||
return cachedBuilder!!
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
startForeground(
|
||||
notificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentVPNPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
@@ -38,93 +60,8 @@ class FlClashService : Service(), BaseServiceInterface {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
|
||||
val intent = Intent(
|
||||
this@FlClashService, MainActivity::class.java
|
||||
)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
stopText, // 使用 suspend 函数获取的文本
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
try {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} catch (_: Exception) {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
override fun onDestroy() {
|
||||
stop()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ class FlClashTileService : TileService() {
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
GlobalState.syncStatus()
|
||||
GlobalState.runState.value?.let { updateTile(it) }
|
||||
GlobalState.runState.observeForever(observer)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -17,18 +12,13 @@ import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.extensions.getIpv4RouteAddress
|
||||
import com.follow.clash.extensions.getIpv6RouteAddress
|
||||
import com.follow.clash.extensions.toCIDR
|
||||
import com.follow.clash.models.AccessControlMode
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -43,6 +33,10 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
if (options.ipv4Address.isNotEmpty()) {
|
||||
val cidr = options.ipv4Address.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
Log.d(
|
||||
"addAddress",
|
||||
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
|
||||
)
|
||||
val routeAddress = options.getIpv4RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
@@ -59,26 +53,39 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
} else {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
if (options.ipv6Address.isNotEmpty()) {
|
||||
val cidr = options.ipv6Address.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute6",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
try {
|
||||
if (options.ipv6Address.isNotEmpty()) {
|
||||
val cidr = options.ipv6Address.toCIDR()
|
||||
Log.d(
|
||||
"addAddress6",
|
||||
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
|
||||
)
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute6",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} else {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
}catch (_:Exception){
|
||||
Log.d(
|
||||
"addAddress6",
|
||||
"IPv6 is not supported."
|
||||
)
|
||||
}
|
||||
addDnsServer(options.dnsServerAddress)
|
||||
setMtu(9000)
|
||||
@@ -128,82 +135,22 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
private var cachedBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
stopText,
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
private suspend fun notificationBuilder(): NotificationCompat.Builder {
|
||||
if (cachedBuilder == null) {
|
||||
cachedBuilder = createFlClashNotificationBuilder().await()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
return cachedBuilder!!
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
startForeground(
|
||||
notificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
try {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} catch (_: Exception) {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
.setContentText(content).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 618 B |
Binary file not shown.
|
Before Width: | Height: | Size: 423 B |
Binary file not shown.
|
Before Width: | Height: | Size: 803 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
17
android/app/src/main/res/drawable/ic.xml
Normal file
17
android/app/src/main/res/drawable/ic.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="240dp"
|
||||
android:height="240dp"
|
||||
android:viewportWidth="240"
|
||||
android:viewportHeight="240"
|
||||
tools:ignore="VectorRaster">
|
||||
<path
|
||||
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
BIN
android/app/src/main/res/mipmap-xhdpi/ic_banner.png
Normal file
BIN
android/app/src/main/res/mipmap-xhdpi/ic_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1,33 +0,0 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = "${kotlin_version}"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$agp_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
project.evaluationDependsOn(':core')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
22
android/build.gradle.kts
Normal file
22
android/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.argumentsWithVarargAsSingleArray
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
@@ -12,11 +12,11 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isJniDebuggable = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@@ -37,16 +37,20 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation("androidx.annotation:annotation-jvm:1.9.1")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyNativeLibs") {
|
||||
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
|
||||
doFirst {
|
||||
delete("src/main/jniLibs")
|
||||
}
|
||||
@@ -54,19 +58,8 @@ tasks.register<Copy>("copyNativeLibs") {
|
||||
into("src/main/jniLibs")
|
||||
}
|
||||
|
||||
tasks.withType<MergeSourceSetFolders>().configureEach {
|
||||
dependsOn("copyNativeLibs")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("assembleDebug").configure {
|
||||
dependsOn("copyNativeLibs")
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(copyNativeLibs)
|
||||
}
|
||||
tasks.named("assembleRelease").configure {
|
||||
dependsOn("copyNativeLibs")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.16.0")
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
|
||||
project("core")
|
||||
|
||||
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
|
||||
|
||||
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
add_compile_options(-O3)
|
||||
@@ -21,11 +23,16 @@ if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
-Wl,--strip-all
|
||||
-Wl,--exclude-libs=ALL
|
||||
)
|
||||
|
||||
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
|
||||
endif ()
|
||||
|
||||
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
|
||||
|
||||
message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
|
||||
if (EXISTS ${LIB_CLASH_PATH})
|
||||
message(STATUS "Found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_definitions(-D_LIBCLASH)
|
||||
message("Found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_compile_definitions(LIBCLASH)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
@@ -34,6 +41,7 @@ if (EXISTS ${LIB_CLASH_PATH})
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
clash)
|
||||
else ()
|
||||
message("Not found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#ifdef LIBCLASH
|
||||
#include <jni.h>
|
||||
|
||||
#ifdef _LIBCLASH
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include "jni_helper.h"
|
||||
#include "libclash.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
|
||||
auto interface = new_global(cb);
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject, const jint fd, jobject cb) {
|
||||
const auto interface = new_global(cb);
|
||||
startTUN(fd, interface);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *) {
|
||||
stopTun();
|
||||
}
|
||||
|
||||
@@ -26,63 +23,50 @@ static jmethodID m_tun_interface_resolve_process;
|
||||
|
||||
static void release_jni_object_impl(void *obj) {
|
||||
ATTACH_JNI();
|
||||
del_global((jobject) obj);
|
||||
del_global(static_cast<jobject>(obj));
|
||||
}
|
||||
|
||||
static void call_tun_interface_protect_impl(void *tun_interface, int fd) {
|
||||
static void call_tun_interface_protect_impl(void *tun_interface, const int fd) {
|
||||
ATTACH_JNI();
|
||||
env->CallVoidMethod((jobject) tun_interface,
|
||||
(jmethodID) m_tun_interface_protect,
|
||||
(jint) fd);
|
||||
env->CallVoidMethod(static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_protect,
|
||||
fd);
|
||||
}
|
||||
|
||||
static const char*
|
||||
static const char *
|
||||
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
|
||||
const char *source,
|
||||
const char *target,
|
||||
int uid) {
|
||||
const char *source,
|
||||
const char *target,
|
||||
const int uid) {
|
||||
ATTACH_JNI();
|
||||
jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface,
|
||||
(jmethodID) m_tun_interface_resolve_process,
|
||||
(jint) protocol,
|
||||
(jstring) new_string(source),
|
||||
(jstring) new_string(target),
|
||||
(jint) uid);
|
||||
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_process,
|
||||
protocol,
|
||||
new_string(source),
|
||||
new_string(target),
|
||||
uid));
|
||||
return get_string(packageName);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
JNI_OnLoad(JavaVM *vm, void *) {
|
||||
JNIEnv *env = nullptr;
|
||||
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
|
||||
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
initialize_jni(vm, env);
|
||||
|
||||
jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface");
|
||||
const auto c_tun_interface = find_class("com/follow/clash/core/TunInterface");
|
||||
|
||||
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
|
||||
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
|
||||
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
|
||||
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
|
||||
|
||||
registerCallbacks(&call_tun_interface_protect_impl,
|
||||
&call_tun_interface_resolve_process_impl,
|
||||
&release_jni_object_impl);
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "jni_helper.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
@@ -12,7 +13,7 @@ static jmethodID m_get_bytes;
|
||||
void initialize_jni(JavaVM *vm, JNIEnv *env) {
|
||||
global_vm = vm;
|
||||
|
||||
c_string = (jclass) new_global(find_class("java/lang/String"));
|
||||
c_string = reinterpret_cast<jclass>(new_global(find_class("java/lang/String")));
|
||||
m_new_string = find_method(c_string, "<init>", "([B)V");
|
||||
m_get_bytes = find_method(c_string, "getBytes", "()[B");
|
||||
}
|
||||
@@ -22,23 +23,23 @@ JavaVM *global_java_vm() {
|
||||
}
|
||||
|
||||
char *jni_get_string(JNIEnv *env, jstring str) {
|
||||
auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes);
|
||||
int length = env->GetArrayLength(array);
|
||||
char *content = (char *) malloc(length + 1);
|
||||
env->GetByteArrayRegion(array, 0, length, (jbyte *) content);
|
||||
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
|
||||
const int length = env->GetArrayLength(array);
|
||||
const auto content = static_cast<char *>(malloc(length + 1));
|
||||
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
|
||||
content[length] = 0;
|
||||
return content;
|
||||
}
|
||||
|
||||
jstring jni_new_string(JNIEnv *env, const char *str) {
|
||||
auto length = (int) strlen(str);
|
||||
jbyteArray array = env->NewByteArray(length);
|
||||
env->SetByteArrayRegion(array, 0, length, (const jbyte *) str);
|
||||
return (jstring) env->NewObject(c_string, m_new_string, array);
|
||||
const auto length = static_cast<int>(strlen(str));
|
||||
const auto array = env->NewByteArray(length);
|
||||
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
|
||||
return reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
|
||||
}
|
||||
|
||||
int jni_catch_exception(JNIEnv *env) {
|
||||
int result = env->ExceptionCheck();
|
||||
const int result = env->ExceptionCheck();
|
||||
if (result) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
@@ -46,9 +47,9 @@ int jni_catch_exception(JNIEnv *env) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void jni_attach_thread(struct scoped_jni *jni) {
|
||||
void jni_attach_thread(scoped_jni *jni) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) {
|
||||
if (vm->GetEnv(reinterpret_cast<void **>(&jni->env), JNI_VERSION_1_6) == JNI_OK) {
|
||||
jni->require_release = 0;
|
||||
return;
|
||||
}
|
||||
@@ -58,9 +59,9 @@ void jni_attach_thread(struct scoped_jni *jni) {
|
||||
jni->require_release = 1;
|
||||
}
|
||||
|
||||
void jni_detach_thread(struct scoped_jni *jni) {
|
||||
void jni_detach_thread(const scoped_jni *env) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (jni->require_release) {
|
||||
if (env->require_release) {
|
||||
vm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <malloc.h>
|
||||
|
||||
struct scoped_jni {
|
||||
JNIEnv *env;
|
||||
@@ -18,14 +15,14 @@ extern char *jni_get_string(JNIEnv *env, jstring str);
|
||||
|
||||
extern int jni_catch_exception(JNIEnv *env);
|
||||
|
||||
extern void jni_attach_thread(struct scoped_jni *jni);
|
||||
extern void jni_attach_thread(scoped_jni *jni);
|
||||
|
||||
extern void jni_detach_thread(struct scoped_jni *env);
|
||||
extern void jni_detach_thread(const scoped_jni *env);
|
||||
|
||||
extern void release_string(char **str);
|
||||
|
||||
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
|
||||
struct scoped_jni _jni; \
|
||||
scoped_jni _jni{}; \
|
||||
jni_attach_thread(&_jni); \
|
||||
JNIEnv *env = _jni.env
|
||||
|
||||
@@ -36,4 +33,4 @@ extern void release_string(char **str);
|
||||
#define new_global(obj) env->NewGlobalRef(obj)
|
||||
#define del_global(obj) env->DeleteGlobalRef(obj)
|
||||
#define get_string(jstr) jni_get_string(env, jstr)
|
||||
#define new_string(cstr) jni_new_string(env, cstr)
|
||||
#define new_string(cstr) jni_new_string(env, cstr)
|
||||
|
||||
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin_version=1.9.22
|
||||
agp_version=8.9.1
|
||||
agp_version=8.9.2
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "$agp_version" apply false
|
||||
id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
include ':core'
|
||||
29
android/settings.gradle.kts
Normal file
29
android/settings.gradle.kts
Normal file
@@ -0,0 +1,29 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||
}
|
||||
|
||||
|
||||
include(":app")
|
||||
include(":core")
|
||||
@@ -13,7 +13,6 @@
|
||||
"resourcesDesc": "External resource related info",
|
||||
"trafficUsage": "Traffic usage",
|
||||
"coreInfo": "Core info",
|
||||
"nullCoreInfoDesc": "Unable to obtain core info",
|
||||
"networkSpeed": "Network speed",
|
||||
"outboundMode": "Outbound mode",
|
||||
"networkDetection": "Network detection",
|
||||
@@ -22,7 +21,6 @@
|
||||
"noProxy": "No proxy",
|
||||
"noProxyDesc": "Please create a profile or add a valid profile",
|
||||
"nullProfileDesc": "No profile, Please add a profile",
|
||||
"nullLogsDesc": "No logs",
|
||||
"settings": "Settings",
|
||||
"language": "Language",
|
||||
"defaultText": "Default",
|
||||
@@ -149,8 +147,6 @@
|
||||
"addressHelp": "WebDAV server address",
|
||||
"addressTip": "Please enter a valid WebDAV address",
|
||||
"password": "Password",
|
||||
"passwordTip": "Password cannot be empty",
|
||||
"accountTip": "Account cannot be empty",
|
||||
"checkUpdate": "Check for updates",
|
||||
"discoverNewVersion": "Discover the new version",
|
||||
"checkUpdateError": "The current application is already the latest version",
|
||||
@@ -185,8 +181,6 @@
|
||||
"expirationTime": "Expiration time",
|
||||
"connections": "Connections",
|
||||
"connectionsDesc": "View current connections data",
|
||||
"nullRequestsDesc": "No requests",
|
||||
"nullConnectionsDesc": "No connections",
|
||||
"intranetIP": "Intranet IP",
|
||||
"view": "View",
|
||||
"cut": "Cut",
|
||||
@@ -219,7 +213,6 @@
|
||||
"autoCloseConnectionsDesc": "Auto close connections after change node",
|
||||
"onlyStatisticsProxy": "Only statistics proxy",
|
||||
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
|
||||
"deleteProfileTip": "Sure you want to delete the current profile?",
|
||||
"pureBlackMode": "Pure black mode",
|
||||
"keepAliveIntervalDesc": "Tcp keep alive interval",
|
||||
"entries": " entries",
|
||||
@@ -250,7 +243,6 @@
|
||||
"dnsDesc": "Update DNS related settings",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"notEmpty": "Cannot be empty",
|
||||
"hostsDesc": "Add Hosts",
|
||||
"vpnTip": "Changes take effect after restarting the VPN",
|
||||
"vpnEnableDesc": "Auto routes all system traffic through VpnService",
|
||||
@@ -337,15 +329,12 @@
|
||||
"fileIsUpdate": "The file has been modified. Do you want to save the changes?",
|
||||
"profileHasUpdate": "The profile has been modified. Do you want to disable auto update?",
|
||||
"hasCacheChange": "Do you want to cache the changes?",
|
||||
"nullProxies": "No proxies",
|
||||
"copySuccess": "Copy success",
|
||||
"copyLink": "Copy link",
|
||||
"exportFile": "Export file",
|
||||
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
|
||||
"detectionTip": "Relying on third-party api is for reference only",
|
||||
"listen": "Listen",
|
||||
"keyExists": "The current key already exists",
|
||||
"valueExists": "The current value already exists",
|
||||
"undo": "undo",
|
||||
"redo": "redo",
|
||||
"none": "none",
|
||||
@@ -353,24 +342,84 @@
|
||||
"basicConfigDesc": "Modify the basic configuration globally",
|
||||
"selectedCountTitle": "{count} items have been selected",
|
||||
"addRule": "Add rule",
|
||||
"ruleProviderEmptyTip": "Rule provider cannot be empty",
|
||||
"ruleName": "Rule name",
|
||||
"content": "Content",
|
||||
"contentEmptyTip": "Content cannot be empty",
|
||||
"subRule": "Sub rule",
|
||||
"subRuleEmptyTip": "Sub rule content cannot be empty",
|
||||
"ruleTarget": "Rule target",
|
||||
"ruleTargetEmptyTip": "Rule target cannot be empty",
|
||||
"sourceIp": "Source IP",
|
||||
"noResolve": "No resolve IP",
|
||||
"getOriginRules": "Get original rules",
|
||||
"overrideOriginRules": "Override the original rule",
|
||||
"addedOriginRules": "Attach on the original rules",
|
||||
"enableOverride": "Enable override",
|
||||
"deleteRuleTip": "Are you sure you want to delete the selected rule?",
|
||||
"saveChanges": "Do you want to save the changes?",
|
||||
"generalDesc": "Modify general settings",
|
||||
"findProcessModeDesc": "There is a certain performance loss after opening",
|
||||
"tabAnimationDesc": "Effective only in mobile view",
|
||||
"saveTip": "Are you sure you want to save?"
|
||||
"saveTip": "Are you sure you want to save?",
|
||||
"colorSchemes": "Color schemes",
|
||||
"palette": "Palette",
|
||||
"tonalSpotScheme": "TonalSpot",
|
||||
"fidelityScheme": "Fidelity",
|
||||
"monochromeScheme": "Monochrome",
|
||||
"neutralScheme": "Neutral",
|
||||
"vibrantScheme": "Vibrant",
|
||||
"expressiveScheme": "Expressive",
|
||||
"contentScheme": "Content",
|
||||
"rainbowScheme": "Rainbow",
|
||||
"fruitSaladScheme": "FruitSalad",
|
||||
"developerMode": "Developer mode",
|
||||
"developerModeEnableTip": "Developer mode is enabled.",
|
||||
"messageTest": "Message test",
|
||||
"messageTestTip": "This is a message.",
|
||||
"crashTest": "Crash test",
|
||||
"clearData": "Clear Data",
|
||||
"textScale": "Text Scaling",
|
||||
"internet": "Internet",
|
||||
"systemApp": "System APP",
|
||||
"noNetworkApp": "No network APP",
|
||||
"contactMe": "Contact me",
|
||||
"recoveryStrategy": "Recovery strategy",
|
||||
"recoveryStrategy_override": "Override",
|
||||
"recoveryStrategy_compatible": "Compatible",
|
||||
"logsTest": "Logs test",
|
||||
"emptyTip": "{label} cannot be empty",
|
||||
"urlTip": "{label} must be a url",
|
||||
"numberTip": "{label} must be a number",
|
||||
"interval": "Interval",
|
||||
"existsTip": "Current {label} already exists",
|
||||
"deleteTip": "Are you sure you want to delete the current {label}?",
|
||||
"deleteMultipTip": "Are you sure you want to delete the selected {label}?",
|
||||
"nullTip": "No {label} at the moment",
|
||||
"script": "Script",
|
||||
"color": "Color",
|
||||
"rename": "Rename",
|
||||
"unnamed": "Unnamed",
|
||||
"pleaseEnterScriptName": "Please enter a script name",
|
||||
"overrideInvalidTip": "Does not take effect in script mode",
|
||||
"mixedPort": "Mixed Port",
|
||||
"socksPort": "Socks Port",
|
||||
"redirPort": "Redir Port",
|
||||
"tproxyPort": "Tproxy Port",
|
||||
"portTip": "{label} must be between 1024 and 49151",
|
||||
"portConflictTip": "Please enter a different port",
|
||||
"import": "Import",
|
||||
"importFile": "Import from file",
|
||||
"importUrl": "Import from URL",
|
||||
"autoSetSystemDns": "Auto set system DNS",
|
||||
"details": "{label} details",
|
||||
"creationTime": "Creation time",
|
||||
"progress": "Progress",
|
||||
"host": "Host",
|
||||
"destination": "Destination",
|
||||
"destinationGeoIP": "Destination GeoIP",
|
||||
"destinationIPASN": "Destination IPASN",
|
||||
"specialProxy": "Special proxy",
|
||||
"specialRules": "special rules",
|
||||
"remoteDestination": "Remote destination",
|
||||
"networkType": "Network type",
|
||||
"proxyChains": "Proxy chains",
|
||||
"log": "Log",
|
||||
"connection": "Connection",
|
||||
"request": "Request"
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
"resourcesDesc": "外部リソース関連情報",
|
||||
"trafficUsage": "トラフィック使用量",
|
||||
"coreInfo": "コア情報",
|
||||
"nullCoreInfoDesc": "コア情報を取得できません",
|
||||
"networkSpeed": "ネットワーク速度",
|
||||
"outboundMode": "アウトバウンドモード",
|
||||
"networkDetection": "ネットワーク検出",
|
||||
@@ -22,7 +21,6 @@
|
||||
"noProxy": "プロキシなし",
|
||||
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
|
||||
"nullProfileDesc": "プロファイルがありません。追加してください",
|
||||
"nullLogsDesc": "ログがありません",
|
||||
"settings": "設定",
|
||||
"language": "言語",
|
||||
"defaultText": "デフォルト",
|
||||
@@ -149,8 +147,6 @@
|
||||
"addressHelp": "WebDAVサーバーアドレス",
|
||||
"addressTip": "有効なWebDAVアドレスを入力",
|
||||
"password": "パスワード",
|
||||
"passwordTip": "パスワードは必須です",
|
||||
"accountTip": "アカウントは必須です",
|
||||
"checkUpdate": "更新を確認",
|
||||
"discoverNewVersion": "新バージョンを発見",
|
||||
"checkUpdateError": "アプリは最新版です",
|
||||
@@ -185,8 +181,6 @@
|
||||
"expirationTime": "有効期限",
|
||||
"connections": "接続",
|
||||
"connectionsDesc": "現在の接続データを表示",
|
||||
"nullRequestsDesc": "リクエストなし",
|
||||
"nullConnectionsDesc": "接続なし",
|
||||
"intranetIP": "イントラネットIP",
|
||||
"view": "表示",
|
||||
"cut": "切り取り",
|
||||
@@ -219,7 +213,6 @@
|
||||
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
|
||||
"onlyStatisticsProxy": "プロキシのみ統計",
|
||||
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
|
||||
"deleteProfileTip": "現在のプロファイルを削除しますか?",
|
||||
"pureBlackMode": "純黒モード",
|
||||
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
|
||||
"entries": " エントリ",
|
||||
@@ -250,7 +243,6 @@
|
||||
"dnsDesc": "DNS関連設定の更新",
|
||||
"key": "キー",
|
||||
"value": "値",
|
||||
"notEmpty": "空欄不可",
|
||||
"hostsDesc": "ホストを追加",
|
||||
"vpnTip": "変更はVPN再起動後に有効",
|
||||
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
|
||||
@@ -337,15 +329,12 @@
|
||||
"fileIsUpdate": "ファイルが変更されました。保存しますか?",
|
||||
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
|
||||
"hasCacheChange": "変更をキャッシュしますか?",
|
||||
"nullProxies": "プロキシなし",
|
||||
"copySuccess": "コピー成功",
|
||||
"copyLink": "リンクをコピー",
|
||||
"exportFile": "ファイルをエクスポート",
|
||||
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
|
||||
"detectionTip": "サードパーティAPIに依存(参考値)",
|
||||
"listen": "リスン",
|
||||
"keyExists": "現在のキーは既に存在します",
|
||||
"valueExists": "現在の値は既に存在します",
|
||||
"undo": "元に戻す",
|
||||
"redo": "やり直す",
|
||||
"none": "なし",
|
||||
@@ -353,24 +342,85 @@
|
||||
"basicConfigDesc": "基本設定をグローバルに変更",
|
||||
"selectedCountTitle": "{count} 項目が選択されています",
|
||||
"addRule": "ルールを追加",
|
||||
"ruleProviderEmptyTip": "ルールプロバイダーは必須です",
|
||||
"ruleName": "ルール名",
|
||||
"content": "内容",
|
||||
"contentEmptyTip": "内容は必須です",
|
||||
"subRule": "サブルール",
|
||||
"subRuleEmptyTip": "サブルールの内容は必須です",
|
||||
"ruleTarget": "ルール対象",
|
||||
"ruleTargetEmptyTip": "ルール対象は必須です",
|
||||
"sourceIp": "送信元IP",
|
||||
"noResolve": "IPを解決しない",
|
||||
"getOriginRules": "元のルールを取得",
|
||||
"overrideOriginRules": "元のルールを上書き",
|
||||
"addedOriginRules": "元のルールに追加",
|
||||
"enableOverride": "上書きを有効化",
|
||||
"deleteRuleTip": "選択したルールを削除しますか?",
|
||||
"saveChanges": "変更を保存しますか?",
|
||||
"generalDesc": "一般設定を変更",
|
||||
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
|
||||
"tabAnimationDesc": "モバイル表示でのみ有効",
|
||||
"saveTip": "保存してもよろしいですか?"
|
||||
"saveTip": "保存してもよろしいですか?",
|
||||
"colorSchemes": "カラースキーム",
|
||||
"palette": "パレット",
|
||||
"tonalSpotScheme": "トーンスポット",
|
||||
"fidelityScheme": "ハイファイデリティー",
|
||||
"monochromeScheme": "モノクローム",
|
||||
"neutralScheme": "ニュートラル",
|
||||
"vibrantScheme": "ビブラント",
|
||||
"expressiveScheme": "エクスプレッシブ",
|
||||
"contentScheme": "コンテンツテーマ",
|
||||
"rainbowScheme": "レインボー",
|
||||
"fruitSaladScheme": "フルーツサラダ",
|
||||
"developerMode": "デベロッパーモード",
|
||||
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
|
||||
"messageTest": "メッセージテスト",
|
||||
"messageTestTip": "これはメッセージです。",
|
||||
"crashTest": "クラッシュテスト",
|
||||
"clearData": "データを消去",
|
||||
"zoom": "ズーム",
|
||||
"textScale": "テキストスケーリング",
|
||||
"internet": "インターネット",
|
||||
"systemApp": "システムアプリ",
|
||||
"noNetworkApp": "ネットワークなしアプリ",
|
||||
"contactMe": "連絡する",
|
||||
"recoveryStrategy": "リカバリー戦略",
|
||||
"recoveryStrategy_override": "オーバーライド",
|
||||
"recoveryStrategy_compatible": "互換性",
|
||||
"logsTest": "ログテスト",
|
||||
"emptyTip": "{label}は空欄にできません",
|
||||
"urlTip": "{label}はURLである必要があります",
|
||||
"numberTip": "{label}は数字でなければなりません",
|
||||
"interval": "インターバル",
|
||||
"existsTip": "現在の{label}は既に存在しています",
|
||||
"deleteTip": "現在の{label}を削除してもよろしいですか?",
|
||||
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
|
||||
"nullTip": "現在{label}はありません",
|
||||
"script": "スクリプト",
|
||||
"color": "カラー",
|
||||
"rename": "リネーム",
|
||||
"unnamed": "無題",
|
||||
"pleaseEnterScriptName": "スクリプト名を入力してください",
|
||||
"overrideInvalidTip": "スクリプトモードでは有効になりません",
|
||||
"mixedPort": "混合ポート",
|
||||
"socksPort": "Socksポート",
|
||||
"redirPort": "Redirポート",
|
||||
"tproxyPort": "Tproxyポート",
|
||||
"portTip": "{label} は 1024 から 49151 の間でなければなりません",
|
||||
"portConflictTip": "別のポートを入力してください",
|
||||
"import": "インポート",
|
||||
"importFile": "ファイルからインポート",
|
||||
"importUrl": "URLからインポート",
|
||||
"autoSetSystemDns": "オートセットシステムDNS",
|
||||
"details": "{label}詳細",
|
||||
"creationTime": "作成時間",
|
||||
"progress": "進捗",
|
||||
"host": "ホスト",
|
||||
"destination": "宛先",
|
||||
"destinationGeoIP": "宛先地理情報",
|
||||
"destinationIPASN": "宛先IP ASN",
|
||||
"specialProxy": "特殊プロキシ",
|
||||
"specialRules": "特殊ルール",
|
||||
"remoteDestination": "リモート宛先",
|
||||
"networkType": "ネットワーク種別",
|
||||
"proxyChains": "プロキシチェーン",
|
||||
"log": "ログ",
|
||||
"connection": "接続",
|
||||
"request": "リクエスト"
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
"resourcesDesc": "Информация, связанная с внешними ресурсами",
|
||||
"trafficUsage": "Использование трафика",
|
||||
"coreInfo": "Информация о ядре",
|
||||
"nullCoreInfoDesc": "Не удалось получить информацию о ядре",
|
||||
"networkSpeed": "Скорость сети",
|
||||
"outboundMode": "Режим исходящего трафика",
|
||||
"networkDetection": "Обнаружение сети",
|
||||
@@ -22,7 +21,6 @@
|
||||
"noProxy": "Нет прокси",
|
||||
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
|
||||
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
|
||||
"nullLogsDesc": "Нет логов",
|
||||
"settings": "Настройки",
|
||||
"language": "Язык",
|
||||
"defaultText": "По умолчанию",
|
||||
@@ -149,8 +147,6 @@
|
||||
"addressHelp": "Адрес сервера WebDAV",
|
||||
"addressTip": "Пожалуйста, введите действительный адрес WebDAV",
|
||||
"password": "Пароль",
|
||||
"passwordTip": "Пароль не может быть пустым",
|
||||
"accountTip": "Аккаунт не может быть пустым",
|
||||
"checkUpdate": "Проверить обновления",
|
||||
"discoverNewVersion": "Обнаружена новая версия",
|
||||
"checkUpdateError": "Текущее приложение уже является последней версией",
|
||||
@@ -185,8 +181,6 @@
|
||||
"expirationTime": "Время истечения",
|
||||
"connections": "Соединения",
|
||||
"connectionsDesc": "Просмотр текущих данных о соединениях",
|
||||
"nullRequestsDesc": "Нет запросов",
|
||||
"nullConnectionsDesc": "Нет соединений",
|
||||
"intranetIP": "Внутренний IP",
|
||||
"view": "Просмотр",
|
||||
"cut": "Вырезать",
|
||||
@@ -219,7 +213,6 @@
|
||||
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
|
||||
"onlyStatisticsProxy": "Только статистика прокси",
|
||||
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
|
||||
"deleteProfileTip": "Вы уверены, что хотите удалить текущий профиль?",
|
||||
"pureBlackMode": "Чисто черный режим",
|
||||
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
|
||||
"entries": " записей",
|
||||
@@ -250,7 +243,6 @@
|
||||
"dnsDesc": "Обновление настроек, связанных с DNS",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"notEmpty": "Не может быть пустым",
|
||||
"hostsDesc": "Добавить Hosts",
|
||||
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
|
||||
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
|
||||
@@ -337,15 +329,12 @@
|
||||
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
|
||||
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
|
||||
"hasCacheChange": "Хотите сохранить изменения в кэше?",
|
||||
"nullProxies": "Нет прокси",
|
||||
"copySuccess": "Копирование успешно",
|
||||
"copyLink": "Копировать ссылку",
|
||||
"exportFile": "Экспорт файла",
|
||||
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
|
||||
"detectionTip": "Опирается на сторонний API, только для справки",
|
||||
"listen": "Слушать",
|
||||
"keyExists": "Текущий ключ уже существует",
|
||||
"valueExists": "Текущее значение уже существует",
|
||||
"undo": "Отменить",
|
||||
"redo": "Повторить",
|
||||
"none": "Нет",
|
||||
@@ -353,24 +342,85 @@
|
||||
"basicConfigDesc": "Глобальное изменение базовых настроек",
|
||||
"selectedCountTitle": "Выбрано {count} элементов",
|
||||
"addRule": "Добавить правило",
|
||||
"ruleProviderEmptyTip": "Поставщик правил не может быть пустым",
|
||||
"ruleName": "Название правила",
|
||||
"content": "Содержание",
|
||||
"contentEmptyTip": "Содержание не может быть пустым",
|
||||
"subRule": "Подправило",
|
||||
"subRuleEmptyTip": "Содержание подправила не может быть пустым",
|
||||
"ruleTarget": "Цель правила",
|
||||
"ruleTargetEmptyTip": "Цель правила не может быть пустой",
|
||||
"sourceIp": "Исходный IP",
|
||||
"noResolve": "Не разрешать IP",
|
||||
"getOriginRules": "Получить оригинальные правила",
|
||||
"overrideOriginRules": "Переопределить оригинальное правило",
|
||||
"addedOriginRules": "Добавить к оригинальным правилам",
|
||||
"enableOverride": "Включить переопределение",
|
||||
"deleteRuleTip": "Вы уверены, что хотите удалить выбранное правило?",
|
||||
"saveChanges": "Сохранить изменения?",
|
||||
"generalDesc": "Изменение общих настроек",
|
||||
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
|
||||
"tabAnimationDesc": "Действительно только в мобильном виде",
|
||||
"saveTip": "Вы уверены, что хотите сохранить?"
|
||||
"saveTip": "Вы уверены, что хотите сохранить?",
|
||||
"colorSchemes": "Цветовые схемы",
|
||||
"palette": "Палитра",
|
||||
"tonalSpotScheme": "Тональный акцент",
|
||||
"fidelityScheme": "Точная передача",
|
||||
"monochromeScheme": "Монохром",
|
||||
"neutralScheme": "Нейтральные",
|
||||
"vibrantScheme": "Яркие",
|
||||
"expressiveScheme": "Экспрессивные",
|
||||
"contentScheme": "Контентная тема",
|
||||
"rainbowScheme": "Радужные",
|
||||
"fruitSaladScheme": "Фруктовый микс",
|
||||
"developerMode": "Режим разработчика",
|
||||
"developerModeEnableTip": "Режим разработчика активирован.",
|
||||
"messageTest": "Тестирование сообщения",
|
||||
"messageTestTip": "Это сообщение.",
|
||||
"crashTest": "Тест на сбои",
|
||||
"clearData": "Очистить данные",
|
||||
"zoom": "Масштаб",
|
||||
"textScale": "Масштабирование текста",
|
||||
"internet": "Интернет",
|
||||
"systemApp": "Системное приложение",
|
||||
"noNetworkApp": "Приложение без сети",
|
||||
"contactMe": "Свяжитесь со мной",
|
||||
"recoveryStrategy": "Стратегия восстановления",
|
||||
"recoveryStrategy_override": "Переопределение",
|
||||
"recoveryStrategy_compatible": "Совместимый",
|
||||
"logsTest": "Тест журналов",
|
||||
"emptyTip": "{label} не может быть пустым",
|
||||
"urlTip": "{label} должен быть URL",
|
||||
"numberTip": "{label} должно быть числом",
|
||||
"interval": "Интервал",
|
||||
"existsTip": "Текущий {label} уже существует",
|
||||
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
|
||||
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
|
||||
"nullTip": "Сейчас {label} нет",
|
||||
"script": "Скрипт",
|
||||
"color": "Цвет",
|
||||
"rename": "Переименовать",
|
||||
"unnamed": "Без имени",
|
||||
"pleaseEnterScriptName": "Пожалуйста, введите название скрипта",
|
||||
"overrideInvalidTip": "В скриптовом режиме не действует",
|
||||
"mixedPort": "Смешанный порт",
|
||||
"socksPort": "Socks-порт",
|
||||
"redirPort": "Redir-порт",
|
||||
"tproxyPort": "Tproxy-порт",
|
||||
"portTip": "{label} должен быть числом от 1024 до 49151",
|
||||
"portConflictTip": "Введите другой порт",
|
||||
"import": "Импорт",
|
||||
"importFile": "Импорт из файла",
|
||||
"importUrl": "Импорт по URL",
|
||||
"autoSetSystemDns": "Автоматическая настройка системного DNS",
|
||||
"details": "Детали {}",
|
||||
"creationTime": "Время создания",
|
||||
"progress": "Прогресс",
|
||||
"host": "Хост",
|
||||
"destination": "Назначение",
|
||||
"destinationGeoIP": "Геолокация назначения",
|
||||
"destinationIPASN": "ASN назначения",
|
||||
"specialProxy": "Специальный прокси",
|
||||
"specialRules": "Специальные правила",
|
||||
"remoteDestination": "Удалённое назначение",
|
||||
"networkType": "Тип сети",
|
||||
"proxyChains": "Цепочки прокси",
|
||||
"log": "Журнал",
|
||||
"connection": "Соединение",
|
||||
"request": "Запрос"
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
"resourcesDesc": "外部资源相关信息",
|
||||
"trafficUsage": "流量统计",
|
||||
"coreInfo": "内核信息",
|
||||
"nullCoreInfoDesc": "无法获取内核信息",
|
||||
"networkSpeed": "网络速度",
|
||||
"outboundMode": "出站模式",
|
||||
"networkDetection": "网络检测",
|
||||
@@ -22,7 +21,6 @@
|
||||
"noProxy": "暂无代理",
|
||||
"noProxyDesc": "请创建配置文件或者添加有效配置文件",
|
||||
"nullProfileDesc": "没有配置文件,请先添加配置文件",
|
||||
"nullLogsDesc": "暂无日志",
|
||||
"settings": "设置",
|
||||
"language": "语言",
|
||||
"defaultText": "默认",
|
||||
@@ -149,8 +147,6 @@
|
||||
"addressHelp": "WebDAV服务器地址",
|
||||
"addressTip": "请输入有效的WebDAV地址",
|
||||
"password": "密码",
|
||||
"passwordTip": "密码不能为空",
|
||||
"accountTip": "账号不能为空",
|
||||
"checkUpdate": "检查更新",
|
||||
"discoverNewVersion": "发现新版本",
|
||||
"checkUpdateError": "当前应用已经是最新版了",
|
||||
@@ -185,8 +181,6 @@
|
||||
"expirationTime": "到期时间",
|
||||
"connections": "连接",
|
||||
"connectionsDesc": "查看当前连接数据",
|
||||
"nullRequestsDesc": "暂无请求",
|
||||
"nullConnectionsDesc": "暂无连接",
|
||||
"intranetIP": "内网 IP",
|
||||
"view": "查看",
|
||||
"cut": "剪切",
|
||||
@@ -219,7 +213,6 @@
|
||||
"autoCloseConnectionsDesc": "切换节点后自动关闭连接",
|
||||
"onlyStatisticsProxy": "仅统计代理",
|
||||
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
|
||||
"deleteProfileTip": "确定要删除当前配置吗?",
|
||||
"pureBlackMode": "纯黑模式",
|
||||
"keepAliveIntervalDesc": "TCP保持活动间隔",
|
||||
"entries": "个条目",
|
||||
@@ -250,7 +243,6 @@
|
||||
"dnsDesc": "更新DNS相关设置",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"notEmpty": "不能为空",
|
||||
"hostsDesc": "追加Hosts",
|
||||
"vpnTip": "重启VPN后改变生效",
|
||||
"vpnEnableDesc": "通过VpnService自动路由系统所有流量",
|
||||
@@ -337,15 +329,12 @@
|
||||
"fileIsUpdate": "文件有修改,是否保存修改",
|
||||
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
|
||||
"hasCacheChange": "是否缓存修改",
|
||||
"nullProxies": "暂无代理",
|
||||
"copySuccess": "复制成功",
|
||||
"copyLink": "复制链接",
|
||||
"exportFile": "导出文件",
|
||||
"cacheCorrupt": "缓存已损坏,是否清空?",
|
||||
"detectionTip": "依赖第三方api,仅供参考",
|
||||
"listen": "监听",
|
||||
"keyExists": "当前键已存在",
|
||||
"valueExists": "当前值已存在",
|
||||
"undo": "撤销",
|
||||
"redo": "重做",
|
||||
"none": "无",
|
||||
@@ -353,24 +342,85 @@
|
||||
"basicConfigDesc": "全局修改基本配置",
|
||||
"selectedCountTitle": "已选择 {count} 项",
|
||||
"addRule": "添加规则",
|
||||
"ruleProviderEmptyTip": "规则提供者不能为空",
|
||||
"ruleName": "规则名称",
|
||||
"content": "内容",
|
||||
"contentEmptyTip": "内容不能为空",
|
||||
"subRule": "子规则",
|
||||
"subRuleEmptyTip": "子规则内容不能为空",
|
||||
"ruleTarget": "规则目标",
|
||||
"ruleTargetEmptyTip": "规则目标不能为空",
|
||||
"sourceIp": "源IP",
|
||||
"noResolve": "不解析IP",
|
||||
"getOriginRules": "获取原始规则",
|
||||
"overrideOriginRules": "覆盖原始规则",
|
||||
"addedOriginRules": "附加到原始规则",
|
||||
"enableOverride": "启用覆写",
|
||||
"deleteRuleTip": "确定要删除选中的规则吗?",
|
||||
"saveChanges": "是否保存更改?",
|
||||
"generalDesc": "修改通用设置",
|
||||
"findProcessModeDesc": "开启后会有一定性能损耗",
|
||||
"tabAnimationDesc": "仅在移动视图中有效",
|
||||
"saveTip": "确定要保存吗?"
|
||||
"saveTip": "确定要保存吗?",
|
||||
"colorSchemes": "配色方案",
|
||||
"palette": "调色板",
|
||||
"tonalSpotScheme": "调性点缀",
|
||||
"fidelityScheme": "高保真",
|
||||
"monochromeScheme": "单色",
|
||||
"neutralScheme": "中性",
|
||||
"vibrantScheme": "活力",
|
||||
"expressiveScheme": "表现力",
|
||||
"contentScheme": "内容主题",
|
||||
"rainbowScheme": "彩虹",
|
||||
"fruitSaladScheme": "果缤纷",
|
||||
"developerMode": "开发者模式",
|
||||
"developerModeEnableTip": "开发者模式已启用。",
|
||||
"messageTest": "消息测试",
|
||||
"messageTestTip": "这是一条消息。",
|
||||
"crashTest": "崩溃测试",
|
||||
"clearData": "清除数据",
|
||||
"zoom": "缩放",
|
||||
"textScale": "文本缩放",
|
||||
"internet": "互联网",
|
||||
"systemApp": "系统应用",
|
||||
"noNetworkApp": "无网络应用",
|
||||
"contactMe": "联系我",
|
||||
"recoveryStrategy": "恢复策略",
|
||||
"recoveryStrategy_override": "覆盖",
|
||||
"recoveryStrategy_compatible": "兼容",
|
||||
"logsTest": "日志测试",
|
||||
"emptyTip": "{label}不能为空",
|
||||
"urlTip": "{label}必须为URL",
|
||||
"numberTip": "{label}必须为数字",
|
||||
"interval": "间隔",
|
||||
"existsTip": "{label}当前已存在",
|
||||
"deleteTip": "确定删除当前{label}吗?",
|
||||
"deleteMultipTip": "确定删除选中的{label}吗?",
|
||||
"nullTip": "暂无{label}",
|
||||
"script": "脚本",
|
||||
"color": "颜色",
|
||||
"rename": "重命名",
|
||||
"unnamed": "未命名",
|
||||
"pleaseEnterScriptName": "请输入脚本名称",
|
||||
"overrideInvalidTip": "在脚本模式下不生效",
|
||||
"mixedPort": "混合端口",
|
||||
"socksPort": "Socks端口",
|
||||
"redirPort": "Redir端口",
|
||||
"tproxyPort": "Tproxy端口",
|
||||
"portTip": "{label} 必须在 1024 到 49151 之间",
|
||||
"portConflictTip": "请输入不同的端口",
|
||||
"import": "导入",
|
||||
"importFile": "通过文件导入",
|
||||
"importUrl": "通过URL导入",
|
||||
"autoSetSystemDns": "自动设置系统DNS",
|
||||
"details": "{label}详情",
|
||||
"creationTime": "创建时间",
|
||||
"progress": "进度",
|
||||
"host": "主机",
|
||||
"destination": "目标地址",
|
||||
"destinationGeoIP": "目标地理定位",
|
||||
"destinationIPASN": "目标IP ASN",
|
||||
"specialProxy": "特殊代理",
|
||||
"specialRules": "特殊规则",
|
||||
"remoteDestination": "远程目标",
|
||||
"networkType": "网络类型",
|
||||
"proxyChains": "代理链",
|
||||
"log": "日志",
|
||||
"connection": "连接",
|
||||
"request": "请求"
|
||||
}
|
||||
Submodule core/Clash.Meta updated: f19dad529f...34ccd1202b
119
core/action.go
119
core/action.go
@@ -5,16 +5,17 @@ import (
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
DefaultValue interface{} `json:"default-value"`
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ActionResult struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
Port int64
|
||||
}
|
||||
|
||||
func (result ActionResult) Json() ([]byte, error) {
|
||||
@@ -22,99 +23,117 @@ func (result ActionResult) Json() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) getResult(data interface{}) []byte {
|
||||
resultAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, _ := resultAction.Json()
|
||||
return res
|
||||
func (result ActionResult) success(data interface{}) {
|
||||
result.Code = 0
|
||||
result.Data = data
|
||||
result.send()
|
||||
}
|
||||
|
||||
func handleAction(action *Action, result func(data interface{})) {
|
||||
func (result ActionResult) error(data interface{}) {
|
||||
result.Code = -1
|
||||
result.Data = data
|
||||
result.send()
|
||||
}
|
||||
|
||||
func handleAction(action *Action, result ActionResult) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
paramsString := action.Data.(string)
|
||||
result(handleInitClash(paramsString))
|
||||
result.success(handleInitClash(paramsString))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
result(handleGetIsInit())
|
||||
result.success(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
result(true)
|
||||
result.success(true)
|
||||
return
|
||||
case shutdownMethod:
|
||||
result(handleShutdown())
|
||||
result.success(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
result(handleValidateConfig(data))
|
||||
result.success(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
result(handleUpdateConfig(data))
|
||||
result.success(handleUpdateConfig(data))
|
||||
return
|
||||
case setupConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
result.success(handleSetupConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
result(handleGetProxies())
|
||||
result.success(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
result(value)
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
result(handleGetTraffic())
|
||||
result.success(handleGetTraffic())
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
result(handleGetTotalTraffic())
|
||||
result.success(handleGetTotalTraffic())
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
result(true)
|
||||
result.success(true)
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
result(value)
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
result(handleGetConnections())
|
||||
result.success(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
result(handleCloseConnections())
|
||||
result.success(handleCloseConnections())
|
||||
return
|
||||
case resetConnectionsMethod:
|
||||
result.success(handleResetConnections())
|
||||
return
|
||||
case getConfigMethod:
|
||||
path := action.Data.(string)
|
||||
config, err := handleGetConfig(path)
|
||||
if err != nil {
|
||||
result.error(err)
|
||||
return
|
||||
}
|
||||
result.success(config)
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
result(handleCloseConnection(id))
|
||||
result.success(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
result(handleGetExternalProviders())
|
||||
result.success(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
result(handleGetExternalProvider(externalProviderName))
|
||||
result.success(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
result(err.Error())
|
||||
result.success(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
result(value)
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
result(value)
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
@@ -122,56 +141,48 @@ func handleAction(action *Action, result func(data interface{})) {
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
result(err.Error())
|
||||
result.success(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
result(value)
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
result(true)
|
||||
result.success(true)
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
result(true)
|
||||
result.success(true)
|
||||
return
|
||||
case startListenerMethod:
|
||||
result(handleStartListener())
|
||||
result.success(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
result(handleStopListener())
|
||||
result.success(handleStopListener())
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
result(value)
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getProfileMethod:
|
||||
profileId := action.Data.(string)
|
||||
handleGetMemory(func(value string) {
|
||||
result(handleGetProfile(profileId))
|
||||
result.success(value)
|
||||
})
|
||||
return
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
result(true)
|
||||
result.success(true)
|
||||
case crashMethod:
|
||||
result.success(true)
|
||||
handleCrash()
|
||||
default:
|
||||
handle := nextHandle(action, result)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
result(action.DefaultValue)
|
||||
}
|
||||
nextHandle(action, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func protect(callback unsafe.Pointer, fd int) {
|
||||
func Protect(callback unsafe.Pointer, fd int) {
|
||||
if globalCallbacks.protectFunc != nil {
|
||||
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
|
||||
}
|
||||
}
|
||||
|
||||
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
|
||||
func ResolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
|
||||
if globalCallbacks.resolveProcessFunc == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
321
core/common.go
321
core/common.go
@@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
b "bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
@@ -21,31 +22,16 @@ import (
|
||||
"github.com/metacubex/mihomo/log"
|
||||
rp "github.com/metacubex/mihomo/rules/provider"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/samber/lo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func splitByMultipleSeparators(s string) interface{} {
|
||||
isSeparator := func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == ';'
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(s, isSeparator)
|
||||
if len(parts) > 1 {
|
||||
return parts
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
version = 0
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
currentConfig *config.Config
|
||||
version = 0
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
mBatch, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
)
|
||||
|
||||
type ExternalProviders []ExternalProvider
|
||||
@@ -54,55 +40,6 @@ 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 readFile(path string) ([]byte, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func getProfilePath(id string) string {
|
||||
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
|
||||
}
|
||||
|
||||
func getProfileProvidersPath(id string) string {
|
||||
return filepath.Join(constant.Path.HomeDir(), "providers", id)
|
||||
}
|
||||
|
||||
func getRawConfigWithId(id string) *config.RawConfig {
|
||||
path := getProfilePath(id)
|
||||
bytes, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Errorln("profile is not exist")
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
log.Errorln("unmarshalRawConfig error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
for _, mapping := range prof.ProxyProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
}
|
||||
for _, mapping := range prof.RuleProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
}
|
||||
return prof
|
||||
}
|
||||
|
||||
func getExternalProvidersRaw() map[string]cp.Provider {
|
||||
eps := make(map[string]cp.Provider)
|
||||
for n, p := range tunnel.Providers() {
|
||||
@@ -167,143 +104,15 @@ func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
overwriteConfig(prof, cfg)
|
||||
return prof
|
||||
}
|
||||
|
||||
func attachHosts(hosts, patchHosts map[string]any) {
|
||||
for k, v := range patchHosts {
|
||||
if str, ok := v.(string); ok {
|
||||
hosts[k] = splitByMultipleSeparators(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePatchDns(dns config.RawDNS) {
|
||||
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
|
||||
if str, ok := pair.Value.(string); ok {
|
||||
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func overrideRules(rules, patchRules []string) []string {
|
||||
target := ""
|
||||
for _, line := range rules {
|
||||
rule := trimArr(strings.Split(line, ","))
|
||||
if len(rule) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(rule[0], "MATCH") {
|
||||
target = rule[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
return rules
|
||||
}
|
||||
rulesExt := lo.Map(ips, func(ip string, _ int) string {
|
||||
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
|
||||
})
|
||||
return append(append(rulesExt, patchRules...), rules...)
|
||||
}
|
||||
|
||||
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
|
||||
targetConfig.ExternalController = patchConfig.ExternalController
|
||||
targetConfig.ExternalUI = ""
|
||||
targetConfig.Interface = ""
|
||||
targetConfig.ExternalUIURL = ""
|
||||
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
|
||||
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
|
||||
targetConfig.IPv6 = patchConfig.IPv6
|
||||
targetConfig.LogLevel = patchConfig.LogLevel
|
||||
targetConfig.Port = 0
|
||||
targetConfig.SocksPort = 0
|
||||
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
|
||||
targetConfig.MixedPort = patchConfig.MixedPort
|
||||
targetConfig.FindProcessMode = patchConfig.FindProcessMode
|
||||
targetConfig.AllowLan = patchConfig.AllowLan
|
||||
targetConfig.Mode = patchConfig.Mode
|
||||
targetConfig.Tun.Enable = patchConfig.Tun.Enable
|
||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||
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"] = ""
|
||||
}
|
||||
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
if configParams.OverrideDns {
|
||||
updatePatchDns(patchConfig.DNS)
|
||||
targetConfig.DNS = patchConfig.DNS
|
||||
} else {
|
||||
if targetConfig.DNS.Enable == false {
|
||||
targetConfig.DNS.Enable = true
|
||||
}
|
||||
}
|
||||
if configParams.OverrideRule {
|
||||
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
|
||||
} else {
|
||||
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
func patchConfig() {
|
||||
log.Infoln("[Apply] patch")
|
||||
general := currentConfig.General
|
||||
controller := currentConfig.Controller
|
||||
tls := currentConfig.TLS
|
||||
tunnel.SetSniffing(general.Sniffing)
|
||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||
dialer.DefaultInterface.Store(general.Interface)
|
||||
adapter.UnifiedDelay.Store(general.UnifiedDelay)
|
||||
tunnel.SetMode(general.Mode)
|
||||
log.SetLevel(general.LogLevel)
|
||||
resolver.DisableIPv6 = !general.IPv6
|
||||
|
||||
route.ReCreateServer(&route.Config{
|
||||
Addr: controller.ExternalController,
|
||||
TLSAddr: controller.ExternalControllerTLS,
|
||||
UnixAddr: controller.ExternalControllerUnix,
|
||||
PipeAddr: controller.ExternalControllerPipe,
|
||||
Secret: controller.Secret,
|
||||
Certificate: tls.Certificate,
|
||||
PrivateKey: tls.PrivateKey,
|
||||
DohServer: controller.ExternalDohServer,
|
||||
IsDebug: false,
|
||||
Cors: route.Cors{
|
||||
AllowOrigins: controller.Cors.AllowOrigins,
|
||||
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func updateListeners(force bool) {
|
||||
func updateListeners() {
|
||||
if !isRunning {
|
||||
return
|
||||
}
|
||||
general := currentConfig.General
|
||||
listeners := currentConfig.Listeners
|
||||
if force == true {
|
||||
stopListeners()
|
||||
if currentConfig == nil {
|
||||
return
|
||||
}
|
||||
listeners := currentConfig.Listeners
|
||||
general := currentConfig.General
|
||||
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
||||
listener.SetAllowLan(general.AllowLan)
|
||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||
@@ -327,11 +136,7 @@ func stopListeners() {
|
||||
listener.StopListener()
|
||||
}
|
||||
|
||||
func patchSelectGroup() {
|
||||
mapping := configParams.SelectedMap
|
||||
if mapping == nil {
|
||||
return
|
||||
}
|
||||
func patchSelectGroup(mapping map[string]string) {
|
||||
for name, proxy := range tunnel.ProxiesWithProviders() {
|
||||
outbound, ok := proxy.(*adapter.Proxy)
|
||||
if !ok {
|
||||
@@ -352,20 +157,102 @@ func patchSelectGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
func applyConfig(rawConfig *config.RawConfig) error {
|
||||
func defaultSetupParams() *SetupParams {
|
||||
return &SetupParams{
|
||||
Config: config.DefaultRawConfig(),
|
||||
TestURL: "https://www.gstatic.com/generate_204",
|
||||
SelectedMap: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func updateConfig(params *UpdateParams) {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
general := currentConfig.General
|
||||
if params.MixedPort != nil {
|
||||
general.MixedPort = *params.MixedPort
|
||||
}
|
||||
if params.Sniffing != nil {
|
||||
general.Sniffing = *params.Sniffing
|
||||
tunnel.SetSniffing(general.Sniffing)
|
||||
}
|
||||
if params.FindProcessMode != nil {
|
||||
general.FindProcessMode = *params.FindProcessMode
|
||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||
}
|
||||
if params.TCPConcurrent != nil {
|
||||
general.TCPConcurrent = *params.TCPConcurrent
|
||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||
}
|
||||
if params.Interface != nil {
|
||||
general.Interface = *params.Interface
|
||||
dialer.DefaultInterface.Store(general.Interface)
|
||||
}
|
||||
if params.UnifiedDelay != nil {
|
||||
general.UnifiedDelay = *params.UnifiedDelay
|
||||
adapter.UnifiedDelay.Store(general.UnifiedDelay)
|
||||
}
|
||||
if params.Mode != nil {
|
||||
general.Mode = *params.Mode
|
||||
tunnel.SetMode(general.Mode)
|
||||
}
|
||||
if params.LogLevel != nil {
|
||||
general.LogLevel = *params.LogLevel
|
||||
log.SetLevel(general.LogLevel)
|
||||
}
|
||||
if params.IPv6 != nil {
|
||||
general.IPv6 = *params.IPv6
|
||||
resolver.DisableIPv6 = !general.IPv6
|
||||
}
|
||||
if params.ExternalController != nil {
|
||||
currentConfig.Controller.ExternalController = *params.ExternalController
|
||||
route.ReCreateServer(&route.Config{
|
||||
Addr: currentConfig.Controller.ExternalController,
|
||||
})
|
||||
}
|
||||
|
||||
if params.Tun != nil {
|
||||
general.Tun.Enable = params.Tun.Enable
|
||||
general.Tun.AutoRoute = *params.Tun.AutoRoute
|
||||
general.Tun.Device = *params.Tun.Device
|
||||
general.Tun.RouteAddress = *params.Tun.RouteAddress
|
||||
general.Tun.DNSHijack = *params.Tun.DNSHijack
|
||||
general.Tun.Stack = *params.Tun.Stack
|
||||
}
|
||||
|
||||
updateListeners()
|
||||
}
|
||||
|
||||
func setupConfig(params *SetupParams) error {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
var err error
|
||||
currentConfig, err = config.ParseRawConfig(rawConfig)
|
||||
constant.DefaultTestURL = params.TestURL
|
||||
currentConfig, err = config.ParseRawConfig(params.Config)
|
||||
if err != nil {
|
||||
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
if configParams.IsPatch {
|
||||
patchConfig()
|
||||
} else {
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup()
|
||||
}
|
||||
updateListeners(false)
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup(params.SelectedMap)
|
||||
updateListeners()
|
||||
return err
|
||||
}
|
||||
|
||||
func UnmarshalJson(data []byte, v any) error {
|
||||
decoder := json.NewDecoder(b.NewReader(data))
|
||||
decoder.UseNumber()
|
||||
err := decoder.Decode(v)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
P "github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -12,19 +17,34 @@ type InitParams struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
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"`
|
||||
OverrideRule bool `json:"override-rule"`
|
||||
type SetupParams struct {
|
||||
Config *config.RawConfig `json:"config"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL string `json:"test-url"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
ProfileId string `json:"profile-id"`
|
||||
Config config.RawConfig `json:"config" `
|
||||
Params ConfigExtendedParams `json:"params"`
|
||||
type UpdateParams struct {
|
||||
Tun *tunSchema `json:"tun"`
|
||||
AllowLan *bool `json:"allow-lan"`
|
||||
MixedPort *int `json:"mixed-port"`
|
||||
FindProcessMode *P.FindProcessMode `json:"find-process-mode"`
|
||||
Mode *tunnel.TunnelMode `json:"mode"`
|
||||
LogLevel *log.LogLevel `json:"log-level"`
|
||||
IPv6 *bool `json:"ipv6"`
|
||||
Sniffing *bool `json:"sniffing"`
|
||||
TCPConcurrent *bool `json:"tcp-concurrent"`
|
||||
ExternalController *string `json:"external-controller"`
|
||||
Interface *string `json:"interface-name"`
|
||||
UnifiedDelay *bool `json:"unified-delay"`
|
||||
}
|
||||
|
||||
type tunSchema struct {
|
||||
Enable bool `yaml:"enable" json:"enable"`
|
||||
Device *string `yaml:"device" json:"device"`
|
||||
Stack *constant.TUNStack `yaml:"stack" json:"stack"`
|
||||
DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"`
|
||||
AutoRoute *bool `yaml:"auto-route" json:"auto-route"`
|
||||
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
|
||||
}
|
||||
|
||||
type ChangeProxyParams struct {
|
||||
@@ -64,6 +84,7 @@ const (
|
||||
asyncTestDelayMethod Method = "asyncTestDelay"
|
||||
getConnectionsMethod Method = "getConnections"
|
||||
closeConnectionsMethod Method = "closeConnections"
|
||||
resetConnectionsMethod Method = "resetConnectionsMethod"
|
||||
closeConnectionMethod Method = "closeConnection"
|
||||
getExternalProvidersMethod Method = "getExternalProviders"
|
||||
getExternalProviderMethod Method = "getExternalProvider"
|
||||
@@ -81,7 +102,9 @@ const (
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
getProfileMethod Method = "getProfile"
|
||||
crashMethod Method = "crash"
|
||||
setupConfigMethod Method = "setupConfig"
|
||||
getConfigMethod Method = "getConfig"
|
||||
)
|
||||
|
||||
type Method string
|
||||
|
||||
50
core/go.mod
50
core/go.mod
@@ -6,12 +6,12 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require (
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.49.1
|
||||
golang.org/x/sync v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
@@ -20,13 +20,13 @@ require (
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/ebitengine/purego v0.8.3 // indirect
|
||||
github.com/enfein/mieru/v3 v3.13.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
@@ -35,7 +35,7 @@ require (
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.1 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
@@ -50,22 +50,27 @@ require (
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.19.0 // indirect
|
||||
github.com/metacubex/bart v0.20.5 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.1 // indirect
|
||||
github.com/metacubex/chacha v0.1.5 // indirect
|
||||
github.com/metacubex/fswatch v0.1.1 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
|
||||
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
github.com/metacubex/utls v1.6.8-alpha.4 // indirect
|
||||
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 // indirect
|
||||
github.com/metacubex/sing-mux v0.3.2 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d // indirect
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 // indirect
|
||||
github.com/metacubex/sing-vmess v0.2.2 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
|
||||
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
|
||||
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
@@ -77,15 +82,9 @@ require (
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.2 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/samber/lo v1.50.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
@@ -107,7 +106,6 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
|
||||
109
core/go.sum
109
core/go.sum
@@ -1,8 +1,8 @@
|
||||
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
|
||||
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
|
||||
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A=
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
|
||||
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
|
||||
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
|
||||
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
|
||||
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
@@ -26,12 +26,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
|
||||
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
|
||||
@@ -39,8 +39,8 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
|
||||
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
@@ -97,38 +97,49 @@ 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/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
|
||||
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
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.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M=
|
||||
github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
|
||||
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
|
||||
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
|
||||
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
|
||||
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo=
|
||||
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
|
||||
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 h1:Y9ebcKya6ow7VHoESCN5+l4zZvg5eaL2IhI5LLCQxQA=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92/go.mod h1:/squZ38pXrYjqtg8qn+joVvwbpGNYQNp8yxKsMVbCto=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d h1:Ey3A1tA8lVkRbK1FDmwuWj/57Nr8JMdpoVqe45mFzJg=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d/go.mod h1:+ukTd0OPFglT3bnKAYTJWYPbuox6HYNXE235r5tHdUk=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
||||
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 h1:4zWKqxTx75TbfW2EmlQ3hxM6RTRg2PYOAVMCnU4I61I=
|
||||
github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8/go.mod h1:2YywXPWW8Z97kTH7RffOeykKzU+l0aiKlglWV1PAS64=
|
||||
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
|
||||
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
|
||||
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
|
||||
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 h1:HWKsf92BqLYqugATFIJ3hYiEBZ7JF6AoqyvqF39afuI=
|
||||
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
|
||||
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.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
@@ -159,27 +170,12 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
||||
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
||||
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
|
||||
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
|
||||
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
@@ -191,9 +187,16 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
@@ -253,7 +256,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
|
||||
93
core/hub.go
93
core/hub.go
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/metacubex/mihomo/common/observable"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
@@ -28,10 +29,8 @@ import (
|
||||
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
currentConfig *config.Config
|
||||
)
|
||||
|
||||
func handleInitClash(paramsString string) bool {
|
||||
@@ -52,7 +51,8 @@ func handleStartListener() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = true
|
||||
updateListeners(true)
|
||||
updateListeners()
|
||||
resolver.ResetConnection()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -91,30 +91,10 @@ func handleValidateConfig(bytes []byte) string {
|
||||
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 {
|
||||
func handleGetProxies() map[string]constant.Proxy {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
data, err := json.Marshal(tunnel.ProxiesWithProviders())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
return tunnel.ProxiesWithProviders()
|
||||
}
|
||||
|
||||
func handleChangeProxy(data string, fn func(string string)) {
|
||||
@@ -184,21 +164,12 @@ func handleGetTotalTraffic() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleGetProfile(profileId string) string {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
data, err := json.Marshal(prof)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleResetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
|
||||
func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
b.Go(paramsString, func() (bool, error) {
|
||||
mBatch.Go(paramsString, func() (bool, error) {
|
||||
var params = &TestDelayParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
@@ -266,6 +237,11 @@ func handleGetConnections() string {
|
||||
func handleCloseConnections() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
closeConnections()
|
||||
return true
|
||||
}
|
||||
|
||||
func closeConnections() {
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
@@ -273,6 +249,12 @@ func handleCloseConnections() bool {
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func handleResetConnections() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
resolver.ResetConnection()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -442,6 +424,47 @@ func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleGetConfig(path string) (*config.RawConfig, error) {
|
||||
bytes, err := readFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prof, nil
|
||||
}
|
||||
|
||||
func handleCrash() {
|
||||
panic("handle invoke crash")
|
||||
}
|
||||
|
||||
func handleUpdateConfig(bytes []byte) string {
|
||||
var params = &UpdateParams{}
|
||||
err := json.Unmarshal(bytes, params)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
updateConfig(params)
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleSetupConfig(bytes []byte) string {
|
||||
var params = defaultSetupParams()
|
||||
err := UnmarshalJson(bytes, params)
|
||||
if err != nil {
|
||||
log.Errorln("unmarshalRawConfig error %v", err)
|
||||
_ = setupConfig(defaultSetupParams())
|
||||
return err.Error()
|
||||
}
|
||||
err = setupConfig(params)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
|
||||
42
core/lib.go
42
core/lib.go
@@ -39,6 +39,14 @@ func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func (result ActionResult) send() {
|
||||
data, err := result.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(result.Port, string(data))
|
||||
}
|
||||
|
||||
//export invokeAction
|
||||
func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
params := C.GoString(paramsChar)
|
||||
@@ -49,22 +57,38 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(data interface{}) {
|
||||
bridge.SendToPort(i, string(action.getResult(data)))
|
||||
})
|
||||
result := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Port: i,
|
||||
}
|
||||
go handleAction(action, result)
|
||||
}
|
||||
|
||||
func sendMessage(message Message) {
|
||||
if messagePort == -1 {
|
||||
return
|
||||
}
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
result := ActionResult{
|
||||
Method: messageMethod,
|
||||
}.getResult(res)))
|
||||
Port: messagePort,
|
||||
Data: message,
|
||||
}
|
||||
result.send()
|
||||
}
|
||||
|
||||
//export getConfig
|
||||
func getConfig(s *C.char) *C.char {
|
||||
path := C.GoString(s)
|
||||
config, err := handleGetConfig(path)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
marshal, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(marshal))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
|
||||
@@ -58,7 +58,7 @@ func (t *TunHandler) handleProtect(fd int) {
|
||||
return
|
||||
}
|
||||
|
||||
protect(t.callback, fd)
|
||||
Protect(t.callback, fd)
|
||||
}
|
||||
|
||||
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
|
||||
@@ -79,7 +79,7 @@ func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
|
||||
if version < 29 {
|
||||
uid = platform.QuerySocketUidFromProcFs(source, target)
|
||||
}
|
||||
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
|
||||
return ResolveProcess(t.callback, protocol, source.String(), target.String(), uid)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -98,13 +98,13 @@ func handleStopTun() {
|
||||
}
|
||||
}
|
||||
|
||||
func handleStartTun(fd int, callback unsafe.Pointer) bool {
|
||||
func handleStartTun(fd int, callback unsafe.Pointer) {
|
||||
handleStopTun()
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
if fd != 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
tunHandler = &TunHandler{
|
||||
callback: callback,
|
||||
limit: semaphore.NewWeighted(4),
|
||||
@@ -113,13 +113,11 @@ func handleStartTun(fd int, callback unsafe.Pointer) bool {
|
||||
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
tunHandler.listener = tunListener
|
||||
} else {
|
||||
removeTunHook()
|
||||
return false
|
||||
}
|
||||
tunHandler.listener = tunListener
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGetRunTime() string {
|
||||
@@ -190,21 +188,21 @@ func handleGetCurrentProfileName() string {
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
func nextHandle(action *Action, result ActionResult) bool {
|
||||
switch action.Method {
|
||||
case getAndroidVpnOptionsMethod:
|
||||
result(handleGetAndroidVpnOptions())
|
||||
result.success(handleGetAndroidVpnOptions())
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
result(true)
|
||||
result.success(true)
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
result(handleGetRunTime())
|
||||
result.success(handleGetRunTime())
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
result(handleGetCurrentProfileName())
|
||||
result.success(handleGetCurrentProfileName())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -222,13 +220,16 @@ func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.c
|
||||
bridge.SendToPort(i, "init error")
|
||||
}
|
||||
handleSetState(stateParams)
|
||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||
bridge.SendToPort(i, handleSetupConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int, callback unsafe.Pointer) bool {
|
||||
return handleStartTun(int(fd), callback)
|
||||
go func() {
|
||||
handleStartTun(int(fd), callback)
|
||||
}()
|
||||
return true
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
@@ -238,7 +239,9 @@ func getRunTime() *C.char {
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
handleStopTun()
|
||||
go func() {
|
||||
handleStopTun()
|
||||
}()
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
|
||||
@@ -12,14 +12,20 @@ import (
|
||||
|
||||
var conn net.Conn
|
||||
|
||||
func sendMessage(message Message) {
|
||||
res, err := message.Json()
|
||||
func (result ActionResult) send() {
|
||||
data, err := result.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
send(Action{
|
||||
send(data)
|
||||
}
|
||||
|
||||
func sendMessage(message Message) {
|
||||
result := ActionResult{
|
||||
Method: messageMethod,
|
||||
}.getResult(res))
|
||||
Data: message,
|
||||
}
|
||||
result.send()
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
@@ -61,12 +67,15 @@ func startServer(arg string) {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action, func(data interface{}) {
|
||||
send(action.getResult(data))
|
||||
})
|
||||
result := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
}
|
||||
|
||||
go handleAction(action, result)
|
||||
}
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
func nextHandle(action *Action, result ActionResult) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ type AccessControl struct {
|
||||
Mode string `json:"mode"`
|
||||
AcceptList []string `json:"acceptList"`
|
||||
RejectList []string `json:"rejectList"`
|
||||
IsFilterSystemApp bool `json:"isFilterSystemApp"`
|
||||
}
|
||||
|
||||
type AndroidVpnRawOptions struct {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.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/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
import 'pages/pages.dart';
|
||||
|
||||
class Application extends ConsumerStatefulWidget {
|
||||
@@ -27,7 +26,6 @@ class Application extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class ApplicationState extends ConsumerState<Application> {
|
||||
late ColorSchemes systemColorSchemes;
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
|
||||
@@ -43,16 +41,8 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
ColorScheme _getAppColorScheme({
|
||||
required Brightness brightness,
|
||||
int? primaryColor,
|
||||
required ColorSchemes systemColorSchemes,
|
||||
}) {
|
||||
if (primaryColor != null) {
|
||||
return ColorScheme.fromSeed(
|
||||
seedColor: Color(primaryColor),
|
||||
brightness: brightness,
|
||||
);
|
||||
} else {
|
||||
return systemColorSchemes.getColorSchemeForBrightness(brightness);
|
||||
}
|
||||
return ref.read(genColorSchemeProvider(brightness));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -72,7 +62,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_autoUpdateGroupTask() {
|
||||
void _autoUpdateGroupTask() {
|
||||
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.appController.updateGroupsDebounce();
|
||||
@@ -81,14 +71,14 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_autoUpdateProfilesTask() {
|
||||
void _autoUpdateProfilesTask() {
|
||||
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
|
||||
await globalState.appController.autoUpdateProfiles();
|
||||
_autoUpdateProfilesTask();
|
||||
});
|
||||
}
|
||||
|
||||
_buildPlatformState(Widget child) {
|
||||
Widget _buildPlatformState(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
@@ -107,11 +97,14 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildState(Widget child) {
|
||||
Widget _buildState(Widget child) {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
child: ConnectivityManager(
|
||||
onConnectivityChanged: () {
|
||||
onConnectivityChanged: (results) async {
|
||||
if (!results.contains(ConnectivityResult.vpn)) {
|
||||
clashCore.closeConnections();
|
||||
}
|
||||
globalState.appController.updateLocalIp();
|
||||
globalState.appController.addCheckIpNumDebounce();
|
||||
},
|
||||
@@ -121,7 +114,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildPlatformApp(Widget child) {
|
||||
Widget _buildPlatformApp(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: child,
|
||||
@@ -132,7 +125,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_buildApp(Widget child) {
|
||||
Widget _buildApp(Widget child) {
|
||||
return MessageManager(
|
||||
child: ThemeManager(
|
||||
child: child,
|
||||
@@ -140,19 +133,6 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
_updateSystemColorSchemes(
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
) {
|
||||
systemColorSchemes = ColorSchemes(
|
||||
lightColorScheme: lightDynamic,
|
||||
darkColorScheme: darkDynamic,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildPlatformState(
|
||||
@@ -162,51 +142,48 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
final locale =
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return AppEnvManager(
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return AppEnvManager(
|
||||
child: _buildApp(
|
||||
AppSidebarContainer(
|
||||
child: _buildPlatformApp(
|
||||
_buildApp(child!),
|
||||
child!,
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: utils.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child!,
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
|
||||
@@ -17,7 +17,7 @@ class ClashCore {
|
||||
late ClashHandlerInterface clashInterface;
|
||||
|
||||
ClashCore._internal() {
|
||||
if (Platform.isAndroid) {
|
||||
if (system.isAndroid) {
|
||||
clashInterface = clashLib!;
|
||||
} else {
|
||||
clashInterface = clashService!;
|
||||
@@ -66,6 +66,11 @@ class ClashCore {
|
||||
|
||||
Future<bool> init() async {
|
||||
await initGeo();
|
||||
if (globalState.config.appSetting.openLogs) {
|
||||
clashCore.startLog();
|
||||
} else {
|
||||
clashCore.stopLog();
|
||||
}
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return await clashInterface.init(
|
||||
InitParams(
|
||||
@@ -79,7 +84,7 @@ class ClashCore {
|
||||
return await clashInterface.setState(state);
|
||||
}
|
||||
|
||||
shutdown() async {
|
||||
Future<void> shutdown() async {
|
||||
await clashInterface.shutdown();
|
||||
}
|
||||
|
||||
@@ -89,60 +94,64 @@ class ClashCore {
|
||||
return clashInterface.validateConfig(data);
|
||||
}
|
||||
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await clashInterface.updateConfig(updateConfigParams);
|
||||
Future<String> updateConfig(UpdateParams updateParams) async {
|
||||
return await clashInterface.updateConfig(updateParams);
|
||||
}
|
||||
|
||||
Future<String> setupConfig(SetupParams setupParams) async {
|
||||
return await clashInterface.setupConfig(setupParams);
|
||||
}
|
||||
|
||||
Future<List<Group>> getProxiesGroups() async {
|
||||
final proxiesRawString = await clashInterface.getProxies();
|
||||
return Isolate.run<List<Group>>(() {
|
||||
if (proxiesRawString.isEmpty) return [];
|
||||
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
|
||||
if (proxies.isEmpty) return [];
|
||||
final groupNames = [
|
||||
UsedProxy.GLOBAL.name,
|
||||
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
|
||||
final proxy = proxies[e] ?? {};
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']);
|
||||
})
|
||||
];
|
||||
final groupsRaw = groupNames.map((groupName) {
|
||||
final group = proxies[groupName];
|
||||
group["all"] = ((group["all"] ?? []) as List)
|
||||
.map(
|
||||
(name) => proxies[name],
|
||||
)
|
||||
.where((proxy) => proxy != null)
|
||||
.toList();
|
||||
return group;
|
||||
}).toList();
|
||||
return groupsRaw
|
||||
final proxies = await clashInterface.getProxies();
|
||||
if (proxies.isEmpty) return [];
|
||||
final groupNames = [
|
||||
UsedProxy.GLOBAL.name,
|
||||
...(proxies[UsedProxy.GLOBAL.name]['all'] as List).where((e) {
|
||||
final proxy = proxies[e] ?? {};
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']);
|
||||
})
|
||||
];
|
||||
final groupsRaw = groupNames.map((groupName) {
|
||||
final group = proxies[groupName];
|
||||
group['all'] = ((group['all'] ?? []) as List)
|
||||
.map(
|
||||
(e) => Group.fromJson(e),
|
||||
(name) => proxies[name],
|
||||
)
|
||||
.where((proxy) => proxy != null)
|
||||
.toList();
|
||||
});
|
||||
return group;
|
||||
}).toList();
|
||||
return groupsRaw
|
||||
.map(
|
||||
(e) => Group.fromJson(e),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
|
||||
return await clashInterface.changeProxy(changeProxyParams);
|
||||
}
|
||||
|
||||
Future<List<Connection>> getConnections() async {
|
||||
Future<List<TrackerInfo>> 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();
|
||||
return connectionsRaw.map((e) => TrackerInfo.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
closeConnection(String id) {
|
||||
void closeConnection(String id) {
|
||||
clashInterface.closeConnection(id);
|
||||
}
|
||||
|
||||
closeConnections() {
|
||||
void closeConnections() {
|
||||
clashInterface.closeConnections();
|
||||
}
|
||||
|
||||
void resetConnections() {
|
||||
clashInterface.resetConnections();
|
||||
}
|
||||
|
||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProviders();
|
||||
@@ -193,11 +202,11 @@ class ClashCore {
|
||||
return clashInterface.updateExternalProvider(providerName);
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
Future<void> startListener() async {
|
||||
await clashInterface.startListener();
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
Future<void> stopListener() async {
|
||||
await clashInterface.stopListener();
|
||||
}
|
||||
|
||||
@@ -206,6 +215,16 @@ class ClashCore {
|
||||
return Delay.fromJson(json.decode(data));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getConfig(String id) async {
|
||||
final profilePath = await appPath.getProfilePath(id);
|
||||
final res = await clashInterface.getConfig(profilePath);
|
||||
if (res.isSuccess) {
|
||||
return res.data as Map<String, dynamic>;
|
||||
} else {
|
||||
throw res.message;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Traffic> getTraffic() async {
|
||||
final trafficString = await clashInterface.getTraffic();
|
||||
if (trafficString.isEmpty) {
|
||||
@@ -241,31 +260,23 @@ class ClashCore {
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
Future<ClashConfigSnippet?> getProfile(String id) async {
|
||||
final res = await clashInterface.getProfile(id);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
|
||||
}
|
||||
|
||||
resetTraffic() {
|
||||
void resetTraffic() {
|
||||
clashInterface.resetTraffic();
|
||||
}
|
||||
|
||||
startLog() {
|
||||
void startLog() {
|
||||
clashInterface.startLog();
|
||||
}
|
||||
|
||||
stopLog() {
|
||||
void stopLog() {
|
||||
clashInterface.stopLog();
|
||||
}
|
||||
|
||||
requestGc() {
|
||||
clashInterface.forceGc();
|
||||
Future<void> requestGc() async {
|
||||
await clashInterface.forceGc();
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
Future<void> destroy() async {
|
||||
await clashInterface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,18 +154,18 @@ class ClashFFI {
|
||||
late final _setrlimit =
|
||||
_setrlimitPtr.asFunction<int Function(int, ffi.Pointer<rlimit>)>();
|
||||
|
||||
int wait1(
|
||||
int wait$1(
|
||||
ffi.Pointer<ffi.Int> arg0,
|
||||
) {
|
||||
return _wait1(
|
||||
return _wait$1(
|
||||
arg0,
|
||||
);
|
||||
}
|
||||
|
||||
late final _wait1Ptr =
|
||||
late final _wait$1Ptr =
|
||||
_lookup<ffi.NativeFunction<pid_t Function(ffi.Pointer<ffi.Int>)>>('wait');
|
||||
late final _wait1 =
|
||||
_wait1Ptr.asFunction<int Function(ffi.Pointer<ffi.Int>)>();
|
||||
late final _wait$1 =
|
||||
_wait$1Ptr.asFunction<int Function(ffi.Pointer<ffi.Int>)>();
|
||||
|
||||
int waitpid(
|
||||
int arg0,
|
||||
@@ -2518,6 +2518,20 @@ class ClashFFI {
|
||||
late final _invokeAction =
|
||||
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
ffi.Pointer<ffi.Char> getConfig(
|
||||
ffi.Pointer<ffi.Char> s,
|
||||
) {
|
||||
return _getConfig(
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getConfigPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('getConfig');
|
||||
late final _getConfig = _getConfigPtr
|
||||
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void startListener() {
|
||||
return _startListener();
|
||||
}
|
||||
@@ -2639,6 +2653,29 @@ class ClashFFI {
|
||||
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
typedef __int8_t = ffi.SignedChar;
|
||||
typedef Dart__int8_t = int;
|
||||
typedef __uint8_t = ffi.UnsignedChar;
|
||||
typedef Dart__uint8_t = int;
|
||||
typedef __int16_t = ffi.Short;
|
||||
typedef Dart__int16_t = int;
|
||||
typedef __uint16_t = ffi.UnsignedShort;
|
||||
typedef Dart__uint16_t = int;
|
||||
typedef __int32_t = ffi.Int;
|
||||
typedef Dart__int32_t = int;
|
||||
typedef __uint32_t = ffi.UnsignedInt;
|
||||
typedef Dart__uint32_t = int;
|
||||
typedef __int64_t = ffi.LongLong;
|
||||
typedef Dart__int64_t = int;
|
||||
typedef __uint64_t = ffi.UnsignedLongLong;
|
||||
typedef Dart__uint64_t = int;
|
||||
typedef __darwin_intptr_t = ffi.Long;
|
||||
typedef Dart__darwin_intptr_t = int;
|
||||
typedef __darwin_natural_t = ffi.UnsignedInt;
|
||||
typedef Dart__darwin_natural_t = int;
|
||||
typedef __darwin_ct_rune_t = ffi.Int;
|
||||
typedef Dart__darwin_ct_rune_t = int;
|
||||
|
||||
final class __mbstate_t extends ffi.Union {
|
||||
@ffi.Array.multi([128])
|
||||
external ffi.Array<ffi.Char> __mbstate8;
|
||||
@@ -2647,6 +2684,46 @@ final class __mbstate_t extends ffi.Union {
|
||||
external int _mbstateL;
|
||||
}
|
||||
|
||||
typedef __darwin_mbstate_t = __mbstate_t;
|
||||
typedef __darwin_ptrdiff_t = ffi.Long;
|
||||
typedef Dart__darwin_ptrdiff_t = int;
|
||||
typedef __darwin_size_t = ffi.UnsignedLong;
|
||||
typedef Dart__darwin_size_t = int;
|
||||
typedef __builtin_va_list = ffi.Pointer<ffi.Char>;
|
||||
typedef __darwin_va_list = __builtin_va_list;
|
||||
typedef __darwin_wchar_t = ffi.Int;
|
||||
typedef Dart__darwin_wchar_t = int;
|
||||
typedef __darwin_rune_t = __darwin_wchar_t;
|
||||
typedef __darwin_wint_t = ffi.Int;
|
||||
typedef Dart__darwin_wint_t = int;
|
||||
typedef __darwin_clock_t = ffi.UnsignedLong;
|
||||
typedef Dart__darwin_clock_t = int;
|
||||
typedef __darwin_socklen_t = __uint32_t;
|
||||
typedef __darwin_ssize_t = ffi.Long;
|
||||
typedef Dart__darwin_ssize_t = int;
|
||||
typedef __darwin_time_t = ffi.Long;
|
||||
typedef Dart__darwin_time_t = int;
|
||||
typedef __darwin_blkcnt_t = __int64_t;
|
||||
typedef __darwin_blksize_t = __int32_t;
|
||||
typedef __darwin_dev_t = __int32_t;
|
||||
typedef __darwin_fsblkcnt_t = ffi.UnsignedInt;
|
||||
typedef Dart__darwin_fsblkcnt_t = int;
|
||||
typedef __darwin_fsfilcnt_t = ffi.UnsignedInt;
|
||||
typedef Dart__darwin_fsfilcnt_t = int;
|
||||
typedef __darwin_gid_t = __uint32_t;
|
||||
typedef __darwin_id_t = __uint32_t;
|
||||
typedef __darwin_ino64_t = __uint64_t;
|
||||
typedef __darwin_ino_t = __darwin_ino64_t;
|
||||
typedef __darwin_mach_port_name_t = __darwin_natural_t;
|
||||
typedef __darwin_mach_port_t = __darwin_mach_port_name_t;
|
||||
typedef __darwin_mode_t = __uint16_t;
|
||||
typedef __darwin_off_t = __int64_t;
|
||||
typedef __darwin_pid_t = __int32_t;
|
||||
typedef __darwin_sigset_t = __uint32_t;
|
||||
typedef __darwin_suseconds_t = __int32_t;
|
||||
typedef __darwin_uid_t = __uint32_t;
|
||||
typedef __darwin_useconds_t = __uint32_t;
|
||||
|
||||
final class __darwin_pthread_handler_rec extends ffi.Struct {
|
||||
external ffi
|
||||
.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>
|
||||
@@ -2731,6 +2808,48 @@ final class _opaque_pthread_t extends ffi.Struct {
|
||||
external ffi.Array<ffi.Char> __opaque;
|
||||
}
|
||||
|
||||
typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t;
|
||||
typedef __darwin_pthread_cond_t = _opaque_pthread_cond_t;
|
||||
typedef __darwin_pthread_condattr_t = _opaque_pthread_condattr_t;
|
||||
typedef __darwin_pthread_key_t = ffi.UnsignedLong;
|
||||
typedef Dart__darwin_pthread_key_t = int;
|
||||
typedef __darwin_pthread_mutex_t = _opaque_pthread_mutex_t;
|
||||
typedef __darwin_pthread_mutexattr_t = _opaque_pthread_mutexattr_t;
|
||||
typedef __darwin_pthread_once_t = _opaque_pthread_once_t;
|
||||
typedef __darwin_pthread_rwlock_t = _opaque_pthread_rwlock_t;
|
||||
typedef __darwin_pthread_rwlockattr_t = _opaque_pthread_rwlockattr_t;
|
||||
typedef __darwin_pthread_t = ffi.Pointer<_opaque_pthread_t>;
|
||||
typedef __darwin_nl_item = ffi.Int;
|
||||
typedef Dart__darwin_nl_item = int;
|
||||
typedef __darwin_wctrans_t = ffi.Int;
|
||||
typedef Dart__darwin_wctrans_t = int;
|
||||
typedef __darwin_wctype_t = __uint32_t;
|
||||
typedef u_int8_t = ffi.UnsignedChar;
|
||||
typedef Dartu_int8_t = int;
|
||||
typedef u_int16_t = ffi.UnsignedShort;
|
||||
typedef Dartu_int16_t = int;
|
||||
typedef u_int32_t = ffi.UnsignedInt;
|
||||
typedef Dartu_int32_t = int;
|
||||
typedef u_int64_t = ffi.UnsignedLongLong;
|
||||
typedef Dartu_int64_t = int;
|
||||
typedef register_t = ffi.Int64;
|
||||
typedef Dartregister_t = int;
|
||||
typedef user_addr_t = u_int64_t;
|
||||
typedef user_size_t = u_int64_t;
|
||||
typedef user_ssize_t = ffi.Int64;
|
||||
typedef Dartuser_ssize_t = int;
|
||||
typedef user_long_t = ffi.Int64;
|
||||
typedef Dartuser_long_t = int;
|
||||
typedef user_ulong_t = u_int64_t;
|
||||
typedef user_time_t = ffi.Int64;
|
||||
typedef Dartuser_time_t = int;
|
||||
typedef user_off_t = ffi.Int64;
|
||||
typedef Dartuser_off_t = int;
|
||||
typedef syscall_arg_t = u_int64_t;
|
||||
typedef ptrdiff_t = __darwin_ptrdiff_t;
|
||||
typedef rsize_t = __darwin_size_t;
|
||||
typedef wint_t = __darwin_wint_t;
|
||||
|
||||
final class _GoString_ extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Char> p;
|
||||
|
||||
@@ -2738,10 +2857,6 @@ final class _GoString_ extends ffi.Struct {
|
||||
external int n;
|
||||
}
|
||||
|
||||
typedef ptrdiff_t = __darwin_ptrdiff_t;
|
||||
typedef __darwin_ptrdiff_t = ffi.Long;
|
||||
typedef Dart__darwin_ptrdiff_t = int;
|
||||
|
||||
enum idtype_t {
|
||||
P_ALL(0),
|
||||
P_PID(1),
|
||||
@@ -2754,10 +2869,15 @@ enum idtype_t {
|
||||
0 => P_ALL,
|
||||
1 => P_PID,
|
||||
2 => P_PGID,
|
||||
_ => throw ArgumentError("Unknown value for idtype_t: $value"),
|
||||
_ => throw ArgumentError('Unknown value for idtype_t: $value'),
|
||||
};
|
||||
}
|
||||
|
||||
typedef pid_t = __darwin_pid_t;
|
||||
typedef id_t = __darwin_id_t;
|
||||
typedef sig_atomic_t = ffi.Int;
|
||||
typedef Dartsig_atomic_t = int;
|
||||
|
||||
final class __darwin_arm_exception_state extends ffi.Struct {
|
||||
@__uint32_t()
|
||||
external int __exception;
|
||||
@@ -2769,9 +2889,6 @@ final class __darwin_arm_exception_state extends ffi.Struct {
|
||||
external int __far;
|
||||
}
|
||||
|
||||
typedef __uint32_t = ffi.UnsignedInt;
|
||||
typedef Dart__uint32_t = int;
|
||||
|
||||
final class __darwin_arm_exception_state64 extends ffi.Struct {
|
||||
@__uint64_t()
|
||||
external int __far;
|
||||
@@ -2783,9 +2900,6 @@ final class __darwin_arm_exception_state64 extends ffi.Struct {
|
||||
external int __exception;
|
||||
}
|
||||
|
||||
typedef __uint64_t = ffi.UnsignedLongLong;
|
||||
typedef Dart__uint64_t = int;
|
||||
|
||||
final class __darwin_arm_exception_state64_v2 extends ffi.Struct {
|
||||
@__uint64_t()
|
||||
external int __far;
|
||||
@@ -2914,6 +3028,9 @@ final class __darwin_mcontext32 extends ffi.Struct {
|
||||
|
||||
final class __darwin_mcontext64 extends ffi.Opaque {}
|
||||
|
||||
typedef mcontext_t = ffi.Pointer<__darwin_mcontext64>;
|
||||
typedef pthread_attr_t = __darwin_pthread_attr_t;
|
||||
|
||||
final class __darwin_sigaltstack extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Void> ss_sp;
|
||||
|
||||
@@ -2924,8 +3041,7 @@ final class __darwin_sigaltstack extends ffi.Struct {
|
||||
external int ss_flags;
|
||||
}
|
||||
|
||||
typedef __darwin_size_t = ffi.UnsignedLong;
|
||||
typedef Dart__darwin_size_t = int;
|
||||
typedef stack_t = __darwin_sigaltstack;
|
||||
|
||||
final class __darwin_ucontext extends ffi.Struct {
|
||||
@ffi.Int()
|
||||
@@ -2944,7 +3060,9 @@ final class __darwin_ucontext extends ffi.Struct {
|
||||
external ffi.Pointer<__darwin_mcontext64> uc_mcontext;
|
||||
}
|
||||
|
||||
typedef __darwin_sigset_t = __uint32_t;
|
||||
typedef ucontext_t = __darwin_ucontext;
|
||||
typedef sigset_t = __darwin_sigset_t;
|
||||
typedef uid_t = __darwin_uid_t;
|
||||
|
||||
final class sigval extends ffi.Union {
|
||||
@ffi.Int()
|
||||
@@ -2968,9 +3086,6 @@ final class sigevent extends ffi.Struct {
|
||||
external ffi.Pointer<pthread_attr_t> sigev_notify_attributes;
|
||||
}
|
||||
|
||||
typedef pthread_attr_t = __darwin_pthread_attr_t;
|
||||
typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t;
|
||||
|
||||
final class __siginfo extends ffi.Struct {
|
||||
@ffi.Int()
|
||||
external int si_signo;
|
||||
@@ -3001,12 +3116,7 @@ final class __siginfo extends ffi.Struct {
|
||||
external ffi.Array<ffi.UnsignedLong> __pad;
|
||||
}
|
||||
|
||||
typedef pid_t = __darwin_pid_t;
|
||||
typedef __darwin_pid_t = __int32_t;
|
||||
typedef __int32_t = ffi.Int;
|
||||
typedef Dart__int32_t = int;
|
||||
typedef uid_t = __darwin_uid_t;
|
||||
typedef __darwin_uid_t = __uint32_t;
|
||||
typedef siginfo_t = __siginfo;
|
||||
|
||||
final class __sigaction_u extends ffi.Union {
|
||||
external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>
|
||||
@@ -3020,7 +3130,7 @@ final class __sigaction_u extends ffi.Union {
|
||||
}
|
||||
|
||||
final class __sigaction extends ffi.Struct {
|
||||
external __sigaction_u __sigaction_u1;
|
||||
external __sigaction_u __sigaction_u$1;
|
||||
|
||||
external ffi.Pointer<
|
||||
ffi.NativeFunction<
|
||||
@@ -3034,11 +3144,8 @@ final class __sigaction extends ffi.Struct {
|
||||
external int sa_flags;
|
||||
}
|
||||
|
||||
typedef siginfo_t = __siginfo;
|
||||
typedef sigset_t = __darwin_sigset_t;
|
||||
|
||||
final class sigaction extends ffi.Struct {
|
||||
external __sigaction_u __sigaction_u1;
|
||||
external __sigaction_u __sigaction_u$1;
|
||||
|
||||
@sigset_t()
|
||||
external int sa_mask;
|
||||
@@ -3047,6 +3154,10 @@ final class sigaction extends ffi.Struct {
|
||||
external int sa_flags;
|
||||
}
|
||||
|
||||
typedef sig_tFunction = ffi.Void Function(ffi.Int);
|
||||
typedef Dartsig_tFunction = void Function(int);
|
||||
typedef sig_t = ffi.Pointer<ffi.NativeFunction<sig_tFunction>>;
|
||||
|
||||
final class sigvec extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>
|
||||
sv_handler;
|
||||
@@ -3065,6 +3176,43 @@ final class sigstack extends ffi.Struct {
|
||||
external int ss_onstack;
|
||||
}
|
||||
|
||||
typedef int_least8_t = ffi.Int8;
|
||||
typedef Dartint_least8_t = int;
|
||||
typedef int_least16_t = ffi.Int16;
|
||||
typedef Dartint_least16_t = int;
|
||||
typedef int_least32_t = ffi.Int32;
|
||||
typedef Dartint_least32_t = int;
|
||||
typedef int_least64_t = ffi.Int64;
|
||||
typedef Dartint_least64_t = int;
|
||||
typedef uint_least8_t = ffi.Uint8;
|
||||
typedef Dartuint_least8_t = int;
|
||||
typedef uint_least16_t = ffi.Uint16;
|
||||
typedef Dartuint_least16_t = int;
|
||||
typedef uint_least32_t = ffi.Uint32;
|
||||
typedef Dartuint_least32_t = int;
|
||||
typedef uint_least64_t = ffi.Uint64;
|
||||
typedef Dartuint_least64_t = int;
|
||||
typedef int_fast8_t = ffi.Int8;
|
||||
typedef Dartint_fast8_t = int;
|
||||
typedef int_fast16_t = ffi.Int16;
|
||||
typedef Dartint_fast16_t = int;
|
||||
typedef int_fast32_t = ffi.Int32;
|
||||
typedef Dartint_fast32_t = int;
|
||||
typedef int_fast64_t = ffi.Int64;
|
||||
typedef Dartint_fast64_t = int;
|
||||
typedef uint_fast8_t = ffi.Uint8;
|
||||
typedef Dartuint_fast8_t = int;
|
||||
typedef uint_fast16_t = ffi.Uint16;
|
||||
typedef Dartuint_fast16_t = int;
|
||||
typedef uint_fast32_t = ffi.Uint32;
|
||||
typedef Dartuint_fast32_t = int;
|
||||
typedef uint_fast64_t = ffi.Uint64;
|
||||
typedef Dartuint_fast64_t = int;
|
||||
typedef intmax_t = ffi.Long;
|
||||
typedef Dartintmax_t = int;
|
||||
typedef uintmax_t = ffi.UnsignedLong;
|
||||
typedef Dartuintmax_t = int;
|
||||
|
||||
final class timeval extends ffi.Struct {
|
||||
@__darwin_time_t()
|
||||
external int tv_sec;
|
||||
@@ -3073,9 +3221,7 @@ final class timeval extends ffi.Struct {
|
||||
external int tv_usec;
|
||||
}
|
||||
|
||||
typedef __darwin_time_t = ffi.Long;
|
||||
typedef Dart__darwin_time_t = int;
|
||||
typedef __darwin_suseconds_t = __int32_t;
|
||||
typedef rlim_t = __uint64_t;
|
||||
|
||||
final class rusage extends ffi.Struct {
|
||||
external timeval ru_utime;
|
||||
@@ -3125,6 +3271,8 @@ final class rusage extends ffi.Struct {
|
||||
external int ru_nivcsw;
|
||||
}
|
||||
|
||||
typedef rusage_info_t = ffi.Pointer<ffi.Void>;
|
||||
|
||||
final class rusage_info_v0 extends ffi.Struct {
|
||||
@ffi.Array.multi([16])
|
||||
external ffi.Array<ffi.Uint8> ri_uuid;
|
||||
@@ -3730,6 +3878,8 @@ final class rusage_info_v6 extends ffi.Struct {
|
||||
external ffi.Array<ffi.Uint64> ri_reserved;
|
||||
}
|
||||
|
||||
typedef rusage_info_current = rusage_info_v6;
|
||||
|
||||
final class rlimit extends ffi.Struct {
|
||||
@rlim_t()
|
||||
external int rlim_cur;
|
||||
@@ -3738,8 +3888,6 @@ final class rlimit extends ffi.Struct {
|
||||
external int rlim_max;
|
||||
}
|
||||
|
||||
typedef rlim_t = __uint64_t;
|
||||
|
||||
final class proc_rlimit_control_wakeupmon extends ffi.Struct {
|
||||
@ffi.Uint32()
|
||||
external int wm_flags;
|
||||
@@ -3748,11 +3896,11 @@ final class proc_rlimit_control_wakeupmon extends ffi.Struct {
|
||||
external int wm_rate;
|
||||
}
|
||||
|
||||
typedef id_t = __darwin_id_t;
|
||||
typedef __darwin_id_t = __uint32_t;
|
||||
|
||||
final class wait extends ffi.Opaque {}
|
||||
|
||||
typedef ct_rune_t = __darwin_ct_rune_t;
|
||||
typedef rune_t = __darwin_rune_t;
|
||||
|
||||
final class div_t extends ffi.Struct {
|
||||
@ffi.Int()
|
||||
external int quot;
|
||||
@@ -3784,18 +3932,18 @@ final class _malloc_zone_t extends ffi.Opaque {}
|
||||
|
||||
typedef malloc_zone_t = _malloc_zone_t;
|
||||
typedef dev_t = __darwin_dev_t;
|
||||
typedef __darwin_dev_t = __int32_t;
|
||||
typedef mode_t = __darwin_mode_t;
|
||||
typedef __darwin_mode_t = __uint16_t;
|
||||
typedef __uint16_t = ffi.UnsignedShort;
|
||||
typedef Dart__uint16_t = int;
|
||||
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
|
||||
typedef release_object_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
typedef Dartrelease_object_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
typedef release_object_func
|
||||
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
|
||||
typedef protect_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface, ffi.Int fd);
|
||||
typedef Dartprotect_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface, int fd);
|
||||
typedef resolve_process_func
|
||||
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
|
||||
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
|
||||
typedef resolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Void> tun_interface,
|
||||
ffi.Int protocol,
|
||||
@@ -3808,12 +3956,35 @@ typedef Dartresolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Char> source,
|
||||
ffi.Pointer<ffi.Char> target,
|
||||
int uid);
|
||||
typedef release_object_func
|
||||
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
|
||||
typedef release_object_funcFunction = ffi.Void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
typedef Dartrelease_object_funcFunction = void Function(
|
||||
ffi.Pointer<ffi.Void> obj);
|
||||
typedef resolve_process_func
|
||||
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
|
||||
typedef GoInt8 = ffi.SignedChar;
|
||||
typedef DartGoInt8 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
typedef GoInt16 = ffi.Short;
|
||||
typedef DartGoInt16 = int;
|
||||
typedef GoUint16 = ffi.UnsignedShort;
|
||||
typedef DartGoUint16 = int;
|
||||
typedef GoInt32 = ffi.Int;
|
||||
typedef DartGoInt32 = int;
|
||||
typedef GoUint32 = ffi.UnsignedInt;
|
||||
typedef DartGoUint32 = int;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint64 = ffi.UnsignedLongLong;
|
||||
typedef DartGoUint64 = int;
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoUint = GoUint64;
|
||||
typedef GoUintptr = ffi.Size;
|
||||
typedef DartGoUintptr = int;
|
||||
typedef GoFloat32 = ffi.Float;
|
||||
typedef DartGoFloat32 = double;
|
||||
typedef GoFloat64 = ffi.Double;
|
||||
typedef DartGoFloat64 = double;
|
||||
typedef GoString = _GoString_;
|
||||
typedef GoMap = ffi.Pointer<ffi.Void>;
|
||||
typedef GoChan = ffi.Pointer<ffi.Void>;
|
||||
|
||||
final class GoInterface extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Void> t;
|
||||
@@ -3831,12 +4002,6 @@ final class GoSlice extends ffi.Struct {
|
||||
external int cap;
|
||||
}
|
||||
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef DartGoInt64 = int;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
typedef DartGoUint8 = int;
|
||||
|
||||
const int __has_safe_buffers = 1;
|
||||
|
||||
const int __DARWIN_ONLY_64_BIT_INO_T = 1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -19,11 +20,15 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<String> validateConfig(String data);
|
||||
|
||||
FutureOr<Result> getConfig(String path);
|
||||
|
||||
Future<String> asyncTestDelay(String url, String proxyName);
|
||||
|
||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
||||
FutureOr<String> updateConfig(UpdateParams updateParams);
|
||||
|
||||
FutureOr<String> getProxies();
|
||||
FutureOr<String> setupConfig(SetupParams setupParams);
|
||||
|
||||
FutureOr<Map> getProxies();
|
||||
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
|
||||
|
||||
@@ -52,11 +57,13 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<String> getMemory();
|
||||
|
||||
resetTraffic();
|
||||
FutureOr<void> resetTraffic();
|
||||
|
||||
startLog();
|
||||
FutureOr<void> startLog();
|
||||
|
||||
stopLog();
|
||||
FutureOr<void> stopLog();
|
||||
|
||||
Future<bool> crash();
|
||||
|
||||
FutureOr<String> getConnections();
|
||||
|
||||
@@ -64,18 +71,12 @@ mixin ClashInterface {
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
|
||||
FutureOr<String> getProfile(String id);
|
||||
FutureOr<bool> resetConnections();
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
Future<bool> setFdMap(int fd);
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
// Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
@@ -88,60 +89,29 @@ mixin AndroidClashInterface {
|
||||
abstract class ClashHandlerInterface with ClashInterface {
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
|
||||
Future.value(false);
|
||||
|
||||
handleResult(ActionResult result) async {
|
||||
Future<void> handleResult(ActionResult result) async {
|
||||
final completer = callbackCompleterMap[result.id];
|
||||
try {
|
||||
switch (result.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:
|
||||
case ActionMethod.setState:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
case ActionMethod.getProxies:
|
||||
case ActionMethod.getTraffic:
|
||||
case ActionMethod.getTotalTraffic:
|
||||
case ActionMethod.asyncTestDelay:
|
||||
case ActionMethod.getConnections:
|
||||
case ActionMethod.getExternalProviders:
|
||||
case ActionMethod.getExternalProvider:
|
||||
case ActionMethod.validateConfig:
|
||||
case ActionMethod.updateConfig:
|
||||
case ActionMethod.updateGeoData:
|
||||
case ActionMethod.updateExternalProvider:
|
||||
case ActionMethod.sideLoadExternalProvider:
|
||||
case ActionMethod.getCountryCode:
|
||||
case ActionMethod.getMemory:
|
||||
completer?.complete(result.data as String);
|
||||
return;
|
||||
case ActionMethod.message:
|
||||
clashMessage.controller.add(result.data as String);
|
||||
clashMessage.controller.add(result.data);
|
||||
completer?.complete(true);
|
||||
return;
|
||||
case ActionMethod.getConfig:
|
||||
completer?.complete(result.toResult);
|
||||
return;
|
||||
default:
|
||||
final isHandled = await nextHandleResult(result, completer);
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(result.data);
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
commonPrint.log(result.id);
|
||||
} catch (e) {
|
||||
commonPrint.log('${result.id} error $e');
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(String message);
|
||||
void sendMessage(String message);
|
||||
|
||||
reStart();
|
||||
FutureOr<void> reStart();
|
||||
|
||||
FutureOr<bool> destroy();
|
||||
|
||||
@@ -150,18 +120,21 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
T? defaultValue,
|
||||
}) async {
|
||||
final id = "${method.name}#${other.id}";
|
||||
final id = '${method.name}#${utils.id}';
|
||||
|
||||
callbackCompleterMap[id] = Completer<T>();
|
||||
|
||||
dynamic defaultValue;
|
||||
|
||||
if (T == String) {
|
||||
defaultValue = "";
|
||||
}
|
||||
if (T == bool) {
|
||||
defaultValue = false;
|
||||
dynamic mDefaultValue = defaultValue;
|
||||
if (mDefaultValue == null) {
|
||||
if (T == String) {
|
||||
mDefaultValue = '';
|
||||
} else if (T == bool) {
|
||||
mDefaultValue = false;
|
||||
} else if (T == Map) {
|
||||
mDefaultValue = {};
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(
|
||||
@@ -170,7 +143,6 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
id: id,
|
||||
method: method,
|
||||
data: data,
|
||||
defaultValue: defaultValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -182,7 +154,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
},
|
||||
onTimeout: onTimeout ??
|
||||
() {
|
||||
return defaultValue;
|
||||
return mDefaultValue;
|
||||
},
|
||||
functionName: id,
|
||||
);
|
||||
@@ -208,6 +180,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
shutdown() async {
|
||||
return await invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
timeout: Duration(seconds: 1),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,17 +207,45 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
Future<String> updateConfig(UpdateParams updateParams) async {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
data: json.encode(updateParams),
|
||||
timeout: Duration(minutes: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
Future<Result> getConfig(String path) async {
|
||||
final res = await invoke<Result>(
|
||||
method: ActionMethod.getConfig,
|
||||
data: path,
|
||||
timeout: Duration(minutes: 2),
|
||||
defaultValue: Result.success({}),
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> setupConfig(SetupParams setupParams) async {
|
||||
final data = await Isolate.run(() => json.encode(setupParams));
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.setupConfig,
|
||||
data: data,
|
||||
timeout: Duration(minutes: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> crash() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.crash,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map> getProxies() {
|
||||
return invoke<Map>(
|
||||
method: ActionMethod.getProxies,
|
||||
timeout: Duration(seconds: 5),
|
||||
);
|
||||
@@ -289,8 +290,8 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.sideLoadExternalProvider,
|
||||
data: json.encode({
|
||||
"providerName": providerName,
|
||||
"data": data,
|
||||
'providerName': providerName,
|
||||
'data': data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -319,17 +320,16 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnection(String id) {
|
||||
Future<bool> resetConnections() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.closeConnection,
|
||||
data: id,
|
||||
method: ActionMethod.resetConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProfile(String id) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProfile,
|
||||
Future<bool> closeConnection(String id) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.closeConnection,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
@@ -382,9 +382,9 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
@override
|
||||
Future<String> asyncTestDelay(String url, String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
"test-url": url,
|
||||
'proxy-name': proxyName,
|
||||
'timeout': httpTimeoutDuration.inMilliseconds,
|
||||
'test-url': url,
|
||||
};
|
||||
return invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/constant.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/plugins/service.dart';
|
||||
@@ -30,7 +29,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
return _canSendCompleter.future;
|
||||
}
|
||||
|
||||
_initService() async {
|
||||
Future<void> _initService() async {
|
||||
await service?.destroy();
|
||||
_registerMainPort(receiverPort.sendPort);
|
||||
receiverPort.listen((message) {
|
||||
@@ -52,7 +51,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
await service?.init();
|
||||
}
|
||||
|
||||
_registerMainPort(SendPort sendPort) {
|
||||
void _registerMainPort(SendPort sendPort) {
|
||||
IsolateNameServer.removePortNameMapping(mainIsolate);
|
||||
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
||||
}
|
||||
@@ -62,26 +61,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> nextHandleResult(result, completer) async {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
return true;
|
||||
case ActionMethod.getRunTime:
|
||||
case ActionMethod.startTun:
|
||||
case ActionMethod.getAndroidVpnOptions:
|
||||
case ActionMethod.getCurrentProfileName:
|
||||
completer?.complete(result.data as String);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
destroy() async {
|
||||
await service?.destroy();
|
||||
@@ -106,22 +85,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
sendPort?.send(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setFdMap(int fd) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setFdMap,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setProcessMap(item) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setProcessMap,
|
||||
data: item,
|
||||
);
|
||||
}
|
||||
|
||||
// @override
|
||||
// Future<bool> stopTun() {
|
||||
// return invoke<bool>(
|
||||
@@ -175,7 +138,7 @@ class ClashLibHandler {
|
||||
late final DynamicLibrary lib;
|
||||
|
||||
ClashLibHandler._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
lib = DynamicLibrary.open('libclash.so');
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
@@ -205,19 +168,19 @@ class ClashLibHandler {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
attachMessagePort(int messagePort) {
|
||||
void attachMessagePort(int messagePort) {
|
||||
clashFFI.attachMessagePort(
|
||||
messagePort,
|
||||
);
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
void updateDns(String dns) {
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
}
|
||||
|
||||
setState(CoreState state) {
|
||||
void setState(CoreState state) {
|
||||
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setState(stateChar);
|
||||
malloc.free(stateChar);
|
||||
@@ -257,19 +220,42 @@ class ClashLibHandler {
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
Future<bool> startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
Future<bool> stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
DateTime? getRunTime() {
|
||||
final runTimeRaw = clashFFI.getRunTime();
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
if (runTimeString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getConfig(String id) async {
|
||||
final path = await appPath.getProfilePath(id);
|
||||
final pathChar = path.toNativeUtf8().cast<Char>();
|
||||
final configRaw = clashFFI.getConfig(pathChar);
|
||||
final configString = configRaw.cast<Utf8>().toDartString();
|
||||
if (configString.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
final config = json.decode(configString);
|
||||
malloc.free(pathChar);
|
||||
clashFFI.freeCString(configRaw);
|
||||
return config;
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
InitParams initParams,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
SetupParams setupParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
@@ -280,7 +266,7 @@ class ClashLibHandler {
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final params = json.encode(setupParams);
|
||||
final initValue = json.encode(initParams);
|
||||
final stateParams = json.encode(state);
|
||||
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
|
||||
@@ -297,15 +283,10 @@ class ClashLibHandler {
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
ClashLib? get clashLib =>
|
||||
Platform.isAndroid && !globalState.isService ? ClashLib() : null;
|
||||
system.isAndroid && !globalState.isService ? ClashLib() : null;
|
||||
|
||||
ClashLibHandler? get clashLibHandler =>
|
||||
system.isAndroid && globalState.isService ? ClashLibHandler() : null;
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ClashMessage {
|
||||
final controller = StreamController<String>();
|
||||
final controller = StreamController<Map<String, Object?>>();
|
||||
|
||||
ClashMessage._() {
|
||||
controller.stream.listen(
|
||||
(message) {
|
||||
if(message.isEmpty){
|
||||
if (message.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final m = AppMessage.fromJson(json.decode(message));
|
||||
final m = AppMessage.fromJson(message);
|
||||
for (final AppMessageListener listener in _listeners) {
|
||||
switch (m.type) {
|
||||
case AppMessageType.log:
|
||||
@@ -24,7 +23,7 @@ class ClashMessage {
|
||||
listener.onDelay(Delay.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.request:
|
||||
listener.onRequest(Connection.fromJson(m.data));
|
||||
listener.onRequest(TrackerInfo.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.loaded:
|
||||
listener.onLoaded(m.data);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -29,9 +28,9 @@ class ClashService extends ClashHandlerInterface {
|
||||
reStart();
|
||||
}
|
||||
|
||||
_initServer() async {
|
||||
Future<void> _initServer() async {
|
||||
runZonedGuarded(() async {
|
||||
final address = !Platform.isWindows
|
||||
final address = !system.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
@@ -51,29 +50,24 @@ class ClashService extends ClashHandlerInterface {
|
||||
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(uint8ListToListIntConverter)
|
||||
.transform(utf8.decoder)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}, (error, stack) {
|
||||
commonPrint.log(error.toString());
|
||||
if(error is SocketException){
|
||||
if (error is SocketException) {
|
||||
globalState.showNotifier(error.toString());
|
||||
globalState.appController.restartCore();
|
||||
// globalState.appController.restartCore();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -89,15 +83,14 @@ class ClashService extends ClashHandlerInterface {
|
||||
await shutdown();
|
||||
}
|
||||
final serverSocket = await serverCompleter.future;
|
||||
final arg = Platform.isWindows
|
||||
? "${serverSocket.port}"
|
||||
final arg = system.isWindows
|
||||
? '${serverSocket.port}'
|
||||
: serverSocket.address.address;
|
||||
bool isSuccess = false;
|
||||
if (Platform.isWindows && await system.checkIsAdmin()) {
|
||||
isSuccess = await request.startCoreByHelper(arg);
|
||||
}
|
||||
if (isSuccess) {
|
||||
return;
|
||||
if (system.isWindows && await system.checkIsAdmin()) {
|
||||
final isSuccess = await request.startCoreByHelper(arg);
|
||||
if (isSuccess) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
process = await Process.start(
|
||||
appPath.corePath,
|
||||
@@ -105,7 +98,13 @@ class ClashService extends ClashHandlerInterface {
|
||||
arg,
|
||||
],
|
||||
);
|
||||
process!.stdout.listen((_) {});
|
||||
process?.stdout.listen((_) {});
|
||||
process?.stderr.listen((e) {
|
||||
final error = utf8.decode(e);
|
||||
if (error.isNotEmpty) {
|
||||
commonPrint.log(error);
|
||||
}
|
||||
});
|
||||
isStarting = false;
|
||||
}
|
||||
|
||||
@@ -123,8 +122,8 @@ class ClashService extends ClashHandlerInterface {
|
||||
socket.writeln(message);
|
||||
}
|
||||
|
||||
_deleteSocketFile() async {
|
||||
if (!Platform.isWindows) {
|
||||
Future<void> _deleteSocketFile() async {
|
||||
if (!system.isWindows) {
|
||||
final file = File(unixSocketPath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
@@ -132,7 +131,7 @@ class ClashService extends ClashHandlerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
_destroySocket() async {
|
||||
Future<void> _destroySocket() async {
|
||||
if (socketCompleter.isCompleted) {
|
||||
final lastSocket = await socketCompleter.future;
|
||||
await lastSocket.close();
|
||||
@@ -142,7 +141,7 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
if (Platform.isWindows) {
|
||||
if (system.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
await _destroySocket();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'system.dart';
|
||||
|
||||
class Android {
|
||||
init() async {
|
||||
Future<void> init() async {
|
||||
app?.onExit = () async {
|
||||
await globalState.appController.savePreferences();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final android = Platform.isAndroid ? Android() : null;
|
||||
final android = system.isAndroid ? Android() : null;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
extension ArchiveExt on Archive {
|
||||
addDirectoryToArchive(String dirPath, String parentPath) {
|
||||
void addDirectoryToArchive(String dirPath, String parentPath) {
|
||||
final dir = Directory(dirPath);
|
||||
final entities = dir.listSync(recursive: false);
|
||||
for (final entity in entities) {
|
||||
@@ -19,7 +20,7 @@ extension ArchiveExt on Archive {
|
||||
}
|
||||
}
|
||||
|
||||
add<T>(String name, T raw) {
|
||||
void add<T>(String name, T raw) {
|
||||
final data = json.encode(raw);
|
||||
addFile(
|
||||
ArchiveFile(name, data.length, data),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ColorExtension on Color {
|
||||
@@ -21,6 +23,10 @@ extension ColorExtension on Color {
|
||||
return withAlpha(77);
|
||||
}
|
||||
|
||||
Color get opacity12 {
|
||||
return withAlpha(31);
|
||||
}
|
||||
|
||||
Color get opacity15 {
|
||||
return withAlpha(38);
|
||||
}
|
||||
@@ -37,11 +43,54 @@ extension ColorExtension on Color {
|
||||
return withAlpha(0);
|
||||
}
|
||||
|
||||
Color darken([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
int get value32bit {
|
||||
return _floatToInt8(a) << 24 |
|
||||
_floatToInt8(r) << 16 |
|
||||
_floatToInt8(g) << 8 |
|
||||
_floatToInt8(b) << 0;
|
||||
}
|
||||
|
||||
int get alpha8bit => (0xff000000 & value32bit) >> 24;
|
||||
|
||||
int get red8bit => (0x00ff0000 & value32bit) >> 16;
|
||||
|
||||
int get green8bit => (0x0000ff00 & value32bit) >> 8;
|
||||
|
||||
int get blue8bit => (0x000000ff & value32bit) >> 0;
|
||||
|
||||
int _floatToInt8(double x) {
|
||||
return (x * 255.0).round() & 0xff;
|
||||
}
|
||||
|
||||
Color lighten([double amount = 10]) {
|
||||
if (amount <= 0) return this;
|
||||
if (amount > 100) return Colors.white;
|
||||
final HSLColor hsl = this == const Color(0xFF000000)
|
||||
? HSLColor.fromColor(this).withSaturation(0)
|
||||
: HSLColor.fromColor(this);
|
||||
return hsl
|
||||
.withLightness(min(1, max(0, hsl.lightness + amount / 100)))
|
||||
.toColor();
|
||||
}
|
||||
|
||||
String get hex {
|
||||
final value = toARGB32();
|
||||
final red = (value >> 16) & 0xFF;
|
||||
final green = (value >> 8) & 0xFF;
|
||||
final blue = value & 0xFF;
|
||||
return '#${red.toRadixString(16).padLeft(2, '0')}'
|
||||
'${green.toRadixString(16).padLeft(2, '0')}'
|
||||
'${blue.toRadixString(16).padLeft(2, '0')}'
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
Color darken([final int amount = 10]) {
|
||||
if (amount <= 0) return this;
|
||||
if (amount > 100) return Colors.black;
|
||||
final HSLColor hsl = HSLColor.fromColor(this);
|
||||
return hsl
|
||||
.withLightness(min(1, max(0, hsl.lightness - amount / 100)))
|
||||
.toColor();
|
||||
}
|
||||
|
||||
Color blendDarken(
|
||||
@@ -74,7 +123,7 @@ extension ColorSchemeExtension on ColorScheme {
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(
|
||||
0.05,
|
||||
5,
|
||||
),
|
||||
)
|
||||
: this;
|
||||
|
||||
@@ -3,7 +3,9 @@ export 'app_localizations.dart';
|
||||
export 'color.dart';
|
||||
export 'constant.dart';
|
||||
export 'context.dart';
|
||||
export 'converter.dart';
|
||||
export 'datetime.dart';
|
||||
export 'fixed.dart';
|
||||
export 'function.dart';
|
||||
export 'future.dart';
|
||||
export 'http.dart';
|
||||
@@ -12,28 +14,26 @@ export 'iterable.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'launch.dart';
|
||||
export 'link.dart';
|
||||
export 'list.dart';
|
||||
export 'lock.dart';
|
||||
export 'measure.dart';
|
||||
export 'mixin.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 'print.dart';
|
||||
export 'protocol.dart';
|
||||
export 'proxy.dart';
|
||||
export 'render.dart';
|
||||
export 'request.dart';
|
||||
export 'scroll.dart';
|
||||
export 'string.dart';
|
||||
export 'system.dart';
|
||||
export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'utils.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
export 'mixin.dart';
|
||||
export 'print.dart';
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
@@ -8,49 +7,48 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const appName = "FlClash";
|
||||
const appHelperService = "FlClashHelperService";
|
||||
const coreName = "clash.meta";
|
||||
const appName = 'FlClash';
|
||||
const appHelperService = 'FlClashHelperService';
|
||||
const coreName = 'clash.meta';
|
||||
const browserUa =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
const packageName = "com.follow.clash";
|
||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
const packageName = 'com.follow.clash';
|
||||
final unixSocketPath = '/tmp/FlClashSocket_${Random().nextInt(10000)}.sock';
|
||||
const helperPort = 47890;
|
||||
const helperTag = "2024125";
|
||||
const baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
const maxTextScale = 1.4;
|
||||
const minTextScale = 0.8;
|
||||
final baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16.ap,
|
||||
horizontal: 16.ap,
|
||||
);
|
||||
|
||||
double textScaleFactor = min(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
1.2,
|
||||
);
|
||||
final defaultTextScaleFactor =
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||
const httpTimeoutDuration = Duration(milliseconds: 5000);
|
||||
const moreDuration = Duration(milliseconds: 100);
|
||||
const animateDuration = Duration(milliseconds: 100);
|
||||
const midDuration = Duration(milliseconds: 200);
|
||||
const commonDuration = Duration(milliseconds: 300);
|
||||
const defaultUpdateDuration = Duration(days: 1);
|
||||
const mmdbFileName = "geoip.metadb";
|
||||
const asnFileName = "ASN.mmdb";
|
||||
const geoIpFileName = "GeoIP.dat";
|
||||
const geoSiteFileName = "GeoSite.dat";
|
||||
const mmdbFileName = 'geoip.metadb';
|
||||
const asnFileName = 'ASN.mmdb';
|
||||
const geoIpFileName = 'GeoIP.dat';
|
||||
const geoSiteFileName = 'GeoSite.dat';
|
||||
final double kHeaderHeight = system.isDesktop
|
||||
? !Platform.isMacOS
|
||||
? !system.isMacOS
|
||||
? 40
|
||||
: 28
|
||||
: 0;
|
||||
const profilesDirectoryName = "profiles";
|
||||
const localhost = "127.0.0.1";
|
||||
const clashConfigKey = "clash_config";
|
||||
const configKey = "config";
|
||||
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
|
||||
const profilesDirectoryName = 'profiles';
|
||||
const localhost = '127.0.0.1';
|
||||
const clashConfigKey = 'clash_config';
|
||||
const configKey = 'config';
|
||||
const double dialogCommonWidth = 300;
|
||||
const repository = "chen08209/FlClash";
|
||||
const defaultExternalController = "127.0.0.1:9090";
|
||||
const repository = 'chen08209/FlClash';
|
||||
const defaultExternalController = '127.0.0.1:9090';
|
||||
const maxMobileWidth = 600;
|
||||
const maxLaptopWidth = 840;
|
||||
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
||||
const defaultTestUrl = 'https://www.gstatic.com/generate_204';
|
||||
final commonFilter = ImageFilter.blur(
|
||||
sigmaX: 5,
|
||||
sigmaY: 5,
|
||||
@@ -58,8 +56,9 @@ final commonFilter = ImageFilter.blur(
|
||||
);
|
||||
|
||||
const navigationItemListEquality = ListEquality<NavigationItem>();
|
||||
const connectionListEquality = ListEquality<Connection>();
|
||||
const trackerInfoListEquality = ListEquality<TrackerInfo>();
|
||||
const stringListEquality = ListEquality<String>();
|
||||
const intListEquality = ListEquality<int>();
|
||||
const logListEquality = ListEquality<Log>();
|
||||
const groupListEquality = ListEquality<Group>();
|
||||
const externalProviderListEquality = ListEquality<ExternalProvider>();
|
||||
@@ -78,12 +77,33 @@ const viewModeColumnsMap = {
|
||||
ViewMode.desktop: [4, 3],
|
||||
};
|
||||
|
||||
const defaultPrimaryColor = Colors.brown;
|
||||
const proxiesListStoreKey = PageStorageKey<String>('proxies_list');
|
||||
const toolsStoreKey = PageStorageKey<String>('tools');
|
||||
const profilesStoreKey = PageStorageKey<String>('profiles');
|
||||
|
||||
const defaultPrimaryColor = 0XFFD8C0C3;
|
||||
|
||||
double getWidgetHeight(num lines) {
|
||||
return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0);
|
||||
return max(lines * 84 + (lines - 1) * 16, 0).ap;
|
||||
}
|
||||
|
||||
final mainIsolate = "FlClashMainIsolate";
|
||||
const maxLength = 1000;
|
||||
|
||||
final serviceIsolate = "FlClashServiceIsolate";
|
||||
final mainIsolate = 'FlClashMainIsolate';
|
||||
|
||||
final serviceIsolate = 'FlClashServiceIsolate';
|
||||
|
||||
const defaultPrimaryColors = [
|
||||
0xFF795548,
|
||||
0xFF03A9F4,
|
||||
0xFFFFFF00,
|
||||
0XFFBBC9CC,
|
||||
0XFFABD397,
|
||||
defaultPrimaryColor,
|
||||
0XFF665390,
|
||||
];
|
||||
|
||||
const scriptTemplate = '''
|
||||
const main = (config) => {
|
||||
return config;
|
||||
}''';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/manager/message_manager.dart';
|
||||
import 'package:fl_clash/widgets/scaffold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -7,10 +7,40 @@ extension BuildContextExtension on BuildContext {
|
||||
return findAncestorStateOfType<CommonScaffoldState>();
|
||||
}
|
||||
|
||||
showNotifier(String text) {
|
||||
Future<void>? showNotifier(String text) {
|
||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
||||
}
|
||||
|
||||
void showSnackBar(
|
||||
String message, {
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
final width = viewWidth;
|
||||
EdgeInsets margin;
|
||||
if (width < 600) {
|
||||
margin = const EdgeInsets.only(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
);
|
||||
} else {
|
||||
margin = EdgeInsets.only(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: width - 316,
|
||||
);
|
||||
}
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
action: action,
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
margin: margin,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Size get appSize {
|
||||
return MediaQuery.of(this).size;
|
||||
}
|
||||
@@ -27,10 +57,10 @@ extension BuildContextExtension on BuildContext {
|
||||
T? state;
|
||||
|
||||
visitor(Element element) {
|
||||
if(!element.mounted){
|
||||
if (!element.mounted) {
|
||||
return;
|
||||
}
|
||||
if(element is StatefulElement){
|
||||
if (element is StatefulElement) {
|
||||
if (element.state is T) {
|
||||
state = element.state as T;
|
||||
}
|
||||
@@ -42,3 +72,18 @@ extension BuildContextExtension on BuildContext {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class BackHandleInherited extends InheritedWidget {
|
||||
final Function handleBack;
|
||||
|
||||
const BackHandleInherited(
|
||||
{super.key, required this.handleBack, required super.child});
|
||||
|
||||
static BackHandleInherited? of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<BackHandleInherited>();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BackHandleInherited oldWidget) {
|
||||
return handleBack != oldWidget.handleBack;
|
||||
}
|
||||
}
|
||||
|
||||
32
lib/common/converter.dart
Normal file
32
lib/common/converter.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class Uint8ListToListIntConverter extends Converter<Uint8List, List<int>> {
|
||||
@override
|
||||
List<int> convert(Uint8List input) {
|
||||
return input.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Sink<Uint8List> startChunkedConversion(Sink<List<int>> sink) {
|
||||
return _Uint8ListToListIntConverterSink(sink);
|
||||
}
|
||||
}
|
||||
|
||||
class _Uint8ListToListIntConverterSink implements Sink<Uint8List> {
|
||||
const _Uint8ListToListIntConverterSink(this._target);
|
||||
|
||||
final Sink<List<int>> _target;
|
||||
|
||||
@override
|
||||
void add(Uint8List data) {
|
||||
_target.add(data.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_target.close();
|
||||
}
|
||||
}
|
||||
|
||||
final uint8ListToListIntConverter = Uint8ListToListIntConverter();
|
||||
@@ -17,26 +17,34 @@ extension DateTimeExtension on DateTime {
|
||||
final difference = currentDateTime.difference(this);
|
||||
final days = difference.inDays;
|
||||
if (days >= 365) {
|
||||
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}";
|
||||
return '${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}';
|
||||
}
|
||||
if (days >= 30) {
|
||||
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}";
|
||||
return '${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}';
|
||||
}
|
||||
if (days >= 1) {
|
||||
return "$days ${appLocalizations.days}${appLocalizations.ago}";
|
||||
return '$days ${appLocalizations.days}${appLocalizations.ago}';
|
||||
}
|
||||
final hours = difference.inHours;
|
||||
if (hours >= 1) {
|
||||
return "$hours ${appLocalizations.hours}${appLocalizations.ago}";
|
||||
return '$hours ${appLocalizations.hours}${appLocalizations.ago}';
|
||||
}
|
||||
final minutes = difference.inMinutes;
|
||||
if (minutes >= 1) {
|
||||
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}";
|
||||
return '$minutes ${appLocalizations.minutes}${appLocalizations.ago}';
|
||||
}
|
||||
return appLocalizations.just;
|
||||
}
|
||||
|
||||
String get show {
|
||||
return toIso8601String().substring(0, 10);
|
||||
return toString().substring(0, 10);
|
||||
}
|
||||
|
||||
String get showFull {
|
||||
return toString().substring(0, 19);
|
||||
}
|
||||
|
||||
String get showTime {
|
||||
return toString().substring(10, 19);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,18 +38,18 @@ class DAVClient {
|
||||
}
|
||||
}
|
||||
|
||||
get root => "/$appName";
|
||||
String get root => '/$appName';
|
||||
|
||||
get backupFile => "$root/$fileName";
|
||||
String get backupFile => '$root/$fileName';
|
||||
|
||||
backup(Uint8List data) async {
|
||||
await client.mkdir("$root");
|
||||
await client.write("$backupFile", data);
|
||||
Future<bool> backup(Uint8List data) async {
|
||||
await client.mkdir(root);
|
||||
await client.write(backupFile, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<List<int>> recovery() async {
|
||||
await client.mkdir("$root");
|
||||
await client.mkdir(root);
|
||||
final data = await client.read(backupFile);
|
||||
return data;
|
||||
}
|
||||
|
||||
81
lib/common/fixed.dart
Normal file
81
lib/common/fixed.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'iterable.dart';
|
||||
|
||||
typedef ValueCallback<T> = T Function();
|
||||
|
||||
class FixedList<T> {
|
||||
final int maxLength;
|
||||
final List<T> _list;
|
||||
|
||||
FixedList(this.maxLength, {List<T>? list})
|
||||
: _list = (list ?? [])..truncate(maxLength);
|
||||
|
||||
void add(T item) {
|
||||
_list.add(item);
|
||||
_list.truncate(maxLength);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_list.clear();
|
||||
}
|
||||
|
||||
List<T> get list => List.unmodifiable(_list);
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
T operator [](int index) => _list[index];
|
||||
|
||||
FixedList<T> copyWith() {
|
||||
return FixedList(
|
||||
maxLength,
|
||||
list: _list,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
int maxLength;
|
||||
late Map<K, V> _map;
|
||||
|
||||
FixedMap(this.maxLength, {Map<K, V>? map}) {
|
||||
_map = map ?? {};
|
||||
}
|
||||
|
||||
V updateCacheValue(K key, ValueCallback<V> callback) {
|
||||
final realValue = _map.updateCacheValue(
|
||||
key,
|
||||
callback,
|
||||
);
|
||||
_adjustMap();
|
||||
return realValue;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_map.clear();
|
||||
}
|
||||
|
||||
void updateMaxLength(int size) {
|
||||
maxLength = size;
|
||||
_adjustMap();
|
||||
}
|
||||
|
||||
void updateMap(Map<K, V> map) {
|
||||
_map = map;
|
||||
_adjustMap();
|
||||
}
|
||||
|
||||
void _adjustMap() {
|
||||
if (_map.length > maxLength) {
|
||||
_map = Map.fromEntries(
|
||||
map.entries.toList()..truncate(maxLength),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
V? get(K key) => _map[key];
|
||||
|
||||
bool containsKey(K key) => _map.containsKey(key);
|
||||
|
||||
int get length => _map.length;
|
||||
|
||||
Map<K, V> get map => Map.unmodifiable(_map);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
class Debouncer {
|
||||
final Map<dynamic, Timer?> _operations = {};
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
|
||||
call(
|
||||
dynamic tag,
|
||||
class Debouncer {
|
||||
final Map<FunctionTag, Timer?> _operations = {};
|
||||
|
||||
void call(
|
||||
FunctionTag tag,
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
@@ -26,17 +28,17 @@ class Debouncer {
|
||||
);
|
||||
}
|
||||
|
||||
cancel(dynamic tag) {
|
||||
void cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Throttler {
|
||||
final Map<dynamic, Timer?> _operations = {};
|
||||
final Map<FunctionTag, Timer?> _operations = {};
|
||||
|
||||
call(
|
||||
dynamic tag,
|
||||
bool call(
|
||||
FunctionTag tag,
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
@@ -59,7 +61,7 @@ class Throttler {
|
||||
return false;
|
||||
}
|
||||
|
||||
cancel(dynamic tag) {
|
||||
void cancel(dynamic tag) {
|
||||
_operations[tag]?.cancel();
|
||||
_operations[tag] = null;
|
||||
}
|
||||
@@ -79,7 +81,7 @@ Future<T> retry<T>({
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
throw "unknown error";
|
||||
throw 'unknown error';
|
||||
}
|
||||
|
||||
final debouncer = Debouncer();
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:ui';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
|
||||
extension CompleterExt<T> on Completer<T> {
|
||||
safeFuture({
|
||||
Future<T> safeFuture({
|
||||
Duration? timeout,
|
||||
VoidCallback? onLast,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
|
||||
@@ -6,18 +6,19 @@ import 'package:fl_clash/state.dart';
|
||||
class FlClashHttpOverrides extends HttpOverrides {
|
||||
static String handleFindProxy(Uri url) {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
return 'DIRECT';
|
||||
}
|
||||
final port = globalState.config.patchClashConfig.mixedPort;
|
||||
final isStart = globalState.appState.runTime != null;
|
||||
commonPrint.log("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
commonPrint.log('find $url proxy:$isStart');
|
||||
if (!isStart) return 'DIRECT';
|
||||
return 'PROXY localhost:$port';
|
||||
}
|
||||
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = super.createHttpClient(context);
|
||||
client.badCertificateCallback = (_, __, ___) => true;
|
||||
client.findProxy = handleFindProxy;
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IconsExt{
|
||||
static const IconData target =
|
||||
IconData(0xe900, fontFamily: "Icons");
|
||||
}
|
||||
class IconsExt {
|
||||
static const IconData target = IconData(0xe900, fontFamily: 'Icons');
|
||||
}
|
||||
|
||||
@@ -38,6 +38,53 @@ extension IterableExt<T> on Iterable<T> {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<T> takeLast({int count = 50}) {
|
||||
if (count <= 0) return Iterable.empty();
|
||||
return count >= length ? this : toList().skip(length - count);
|
||||
}
|
||||
}
|
||||
|
||||
extension ListExt<T> on List<T> {
|
||||
void truncate(int maxLength) {
|
||||
if (maxLength == 0) {
|
||||
return;
|
||||
}
|
||||
if (length > maxLength) {
|
||||
removeRange(0, length - maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
List<T> intersection(List<T> list) {
|
||||
return where((item) => list.contains(item)).toList();
|
||||
}
|
||||
|
||||
List<List<T>> batch(int maxConcurrent) {
|
||||
final batches = (length / maxConcurrent).ceil();
|
||||
final List<List<T>> res = [];
|
||||
for (int i = 0; i < batches; i++) {
|
||||
if (i != batches - 1) {
|
||||
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
|
||||
} else {
|
||||
res.add(sublist(i * maxConcurrent, length));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
List<T> safeSublist(int start, [int? end]) {
|
||||
if (start <= 0) return this;
|
||||
if (start > length) return [];
|
||||
if (end != null) {
|
||||
return sublist(start, end.clamp(start, length));
|
||||
}
|
||||
return sublist(start);
|
||||
}
|
||||
|
||||
T safeGet(int index) {
|
||||
if (length > index) return this[index];
|
||||
return last;
|
||||
}
|
||||
}
|
||||
|
||||
extension DoubleListExt on List<double> {
|
||||
@@ -67,10 +114,10 @@ extension DoubleListExt on List<double> {
|
||||
}
|
||||
|
||||
extension MapExt<K, V> on Map<K, V> {
|
||||
getCacheValue(K key, V defaultValue) {
|
||||
V updateCacheValue(K key, V Function() callback) {
|
||||
if (this[key] == null) {
|
||||
this[key] = defaultValue;
|
||||
this[key] = callback();
|
||||
}
|
||||
return this[key];
|
||||
return this[key]!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:uni_platform/uni_platform.dart';
|
||||
|
||||
import 'system.dart';
|
||||
|
||||
final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
|
||||
<PhysicalKeyboardKey, String>{
|
||||
PhysicalKeyboardKey.keyA: 'A',
|
||||
@@ -79,14 +79,14 @@ final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
|
||||
PhysicalKeyboardKey.arrowLeft: '←',
|
||||
PhysicalKeyboardKey.arrowDown: '↓',
|
||||
PhysicalKeyboardKey.arrowUp: '↑',
|
||||
PhysicalKeyboardKey.controlLeft: "CTRL",
|
||||
PhysicalKeyboardKey.controlLeft: 'CTRL',
|
||||
PhysicalKeyboardKey.shiftLeft: 'SHIFT',
|
||||
PhysicalKeyboardKey.altLeft: "ALT",
|
||||
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '⌘' : 'WIN',
|
||||
PhysicalKeyboardKey.controlRight: "CTRL",
|
||||
PhysicalKeyboardKey.altLeft: 'ALT',
|
||||
PhysicalKeyboardKey.metaLeft: system.isMacOS ? '⌘' : 'WIN',
|
||||
PhysicalKeyboardKey.controlRight: 'CTRL',
|
||||
PhysicalKeyboardKey.shiftRight: 'SHIFT',
|
||||
PhysicalKeyboardKey.altRight: "ALT",
|
||||
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '⌘' : 'WIN',
|
||||
PhysicalKeyboardKey.altRight: 'ALT',
|
||||
PhysicalKeyboardKey.metaRight: system.isMacOS ? '⌘' : 'WIN',
|
||||
PhysicalKeyboardKey.fn: 'FN',
|
||||
};
|
||||
|
||||
@@ -101,6 +101,3 @@ extension KeyboardKeyExt on KeyboardKey {
|
||||
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
@@ -33,7 +34,10 @@ class AutoLaunch {
|
||||
return await launchAtStartup.disable();
|
||||
}
|
||||
|
||||
updateStatus(bool isAutoLaunch) async {
|
||||
Future<void> updateStatus(bool isAutoLaunch) async {
|
||||
if (kDebugMode) {
|
||||
return;
|
||||
}
|
||||
if (await isEnable == isAutoLaunch) return;
|
||||
if (isAutoLaunch == true) {
|
||||
enable();
|
||||
|
||||
@@ -15,8 +15,9 @@ class LinkManager {
|
||||
_appLinks = AppLinks();
|
||||
}
|
||||
|
||||
initAppLinksListen(installConfigCallBack) async {
|
||||
commonPrint.log("initAppLinksListen");
|
||||
Future<void> initAppLinksListen(
|
||||
Function(String url) installConfigCallBack) async {
|
||||
commonPrint.log('initAppLinksListen');
|
||||
destroy();
|
||||
subscription = _appLinks.uriLinkStream.listen(
|
||||
(uri) {
|
||||
@@ -32,7 +33,7 @@ class LinkManager {
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
void destroy() {
|
||||
if (subscription != null) {
|
||||
subscription?.cancel();
|
||||
subscription = null;
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import 'dart:collection';
|
||||
|
||||
class FixedList<T> {
|
||||
final int maxLength;
|
||||
final List<T> _list;
|
||||
|
||||
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
|
||||
|
||||
add(T item) {
|
||||
if (_list.length == maxLength) {
|
||||
_list.removeAt(0);
|
||||
}
|
||||
_list.add(item);
|
||||
}
|
||||
|
||||
clear() {
|
||||
_list.clear();
|
||||
}
|
||||
|
||||
List<T> get list => List.unmodifiable(_list);
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
T operator [](int index) => _list[index];
|
||||
|
||||
FixedList<T> copyWith() {
|
||||
return FixedList(
|
||||
maxLength,
|
||||
list: _list,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
int maxSize;
|
||||
final Map<K, V> _map = {};
|
||||
final Queue<K> _queue = Queue<K>();
|
||||
|
||||
FixedMap(this.maxSize);
|
||||
|
||||
put(K key, V value) {
|
||||
if (_map.length == maxSize) {
|
||||
final oldestKey = _queue.removeFirst();
|
||||
_map.remove(oldestKey);
|
||||
}
|
||||
_map[key] = value;
|
||||
_queue.add(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
clear() {
|
||||
_map.clear();
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
updateMaxSize(int size){
|
||||
maxSize = size;
|
||||
}
|
||||
|
||||
V? get(K key) => _map[key];
|
||||
|
||||
|
||||
bool containsKey(K key) => _map.containsKey(key);
|
||||
|
||||
int get length => _map.length;
|
||||
|
||||
Map<K, V> get map => Map.unmodifiable(_map);
|
||||
}
|
||||
|
||||
extension ListExtension<T> on List<T> {
|
||||
List<T> intersection(List<T> list) {
|
||||
return where((item) => list.contains(item)).toList();
|
||||
}
|
||||
|
||||
List<List<T>> batch(int maxConcurrent) {
|
||||
final batches = (length / maxConcurrent).ceil();
|
||||
final List<List<T>> res = [];
|
||||
for (int i = 0; i < batches; i++) {
|
||||
if (i != batches - 1) {
|
||||
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
|
||||
} else {
|
||||
res.add(sublist(i * maxConcurrent, length));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
List<T> safeSublist(int start) {
|
||||
if (start <= 0) return this;
|
||||
if (start > length) return [];
|
||||
return sublist(start);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Measure {
|
||||
final TextScaler _textScale;
|
||||
final TextScaler _textScaler;
|
||||
final BuildContext context;
|
||||
final Map<String, dynamic> _measureMap;
|
||||
|
||||
Measure.of(this.context)
|
||||
: _textScale = TextScaler.linear(
|
||||
Measure.of(this.context, double textScaleFactor)
|
||||
: _measureMap = {},
|
||||
_textScaler = TextScaler.linear(
|
||||
textScaleFactor,
|
||||
);
|
||||
|
||||
@@ -21,7 +23,7 @@ class Measure {
|
||||
style: text.style,
|
||||
),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScale,
|
||||
textScaler: _textScaler,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
)..layout(
|
||||
maxWidth: maxWidth,
|
||||
@@ -29,81 +31,87 @@ class Measure {
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
double? _bodyMediumHeight;
|
||||
Size? _bodyLargeSize;
|
||||
double? _bodySmallHeight;
|
||||
double? _labelSmallHeight;
|
||||
double? _labelMediumHeight;
|
||||
double? _titleLargeHeight;
|
||||
double? _titleMediumHeight;
|
||||
|
||||
double get bodyMediumHeight {
|
||||
_bodyMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
).height;
|
||||
return _bodyMediumHeight!;
|
||||
return _measureMap.updateCacheValue(
|
||||
'bodyMediumHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
Size get bodyLargeSize {
|
||||
_bodyLargeSize ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
double get bodyLargeHeight {
|
||||
return _measureMap.updateCacheValue(
|
||||
'bodyLargeHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
return _bodyLargeSize!;
|
||||
}
|
||||
|
||||
double get bodySmallHeight {
|
||||
_bodySmallHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
).height;
|
||||
return _bodySmallHeight!;
|
||||
return _measureMap.updateCacheValue(
|
||||
'bodySmallHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get labelSmallHeight {
|
||||
_labelSmallHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
).height;
|
||||
return _labelSmallHeight!;
|
||||
return _measureMap.updateCacheValue(
|
||||
'labelSmallHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get labelMediumHeight {
|
||||
_labelMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.labelMedium,
|
||||
),
|
||||
).height;
|
||||
return _labelMediumHeight!;
|
||||
return _measureMap.updateCacheValue(
|
||||
'labelMediumHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.labelMedium,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get titleLargeHeight {
|
||||
_titleLargeHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
).height;
|
||||
return _titleLargeHeight!;
|
||||
return _measureMap.updateCacheValue(
|
||||
'titleLargeHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get titleMediumHeight {
|
||||
_titleMediumHeight ??= computeTextSize(
|
||||
Text(
|
||||
"X",
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
).height;
|
||||
return _titleMediumHeight!;
|
||||
return _measureMap.updateCacheValue(
|
||||
'titleMediumHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'context.dart';
|
||||
|
||||
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
|
||||
set value(T value) {
|
||||
@@ -17,37 +14,31 @@ mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
|
||||
return res;
|
||||
}
|
||||
|
||||
onUpdate(T value) {}
|
||||
void onUpdate(T value) {}
|
||||
}
|
||||
|
||||
mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||
void onPageShow() {
|
||||
initPageState();
|
||||
}
|
||||
|
||||
initPageState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = actions;
|
||||
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
commonScaffoldState?.updateSearchState(
|
||||
(_) => onSearch != null
|
||||
? AppBarSearchState(
|
||||
onSearch: onSearch!,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void onPageHidden() {}
|
||||
|
||||
List<Widget> get actions => [];
|
||||
|
||||
Widget? get floatingActionButton => null;
|
||||
|
||||
Function(String)? get onSearch => null;
|
||||
|
||||
Function(List<String>)? get onKeywordsUpdate => null;
|
||||
}
|
||||
// mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||
// initPageState() {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// final commonScaffoldState = context.commonScaffoldState;
|
||||
// commonScaffoldState?.actions = actions;
|
||||
// commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||
// commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||
// commonScaffoldState?.updateSearchState(
|
||||
// (_) => onSearch != null
|
||||
// ? AppBarSearchState(
|
||||
// onSearch: onSearch!,
|
||||
// )
|
||||
// : null,
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// List<Widget> get actions => [];
|
||||
//
|
||||
// Widget? get floatingActionButton => null;
|
||||
//
|
||||
// Function(String)? get onSearch => null;
|
||||
//
|
||||
// Function(List<String>)? get onKeywordsUpdate => null;
|
||||
// }
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/fragments.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/views/views.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class Navigation {
|
||||
static Navigation? _instance;
|
||||
@@ -11,63 +13,69 @@ class Navigation {
|
||||
bool hasProxies = false,
|
||||
}) {
|
||||
return [
|
||||
const NavigationItem(
|
||||
NavigationItem(
|
||||
keep: false,
|
||||
icon: Icon(Icons.space_dashboard),
|
||||
label: PageLabel.dashboard,
|
||||
keep: false,
|
||||
fragment: DashboardFragment(
|
||||
builder: (_) => const DashboardView(
|
||||
key: GlobalObjectKey(PageLabel.dashboard),
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.article),
|
||||
label: PageLabel.proxies,
|
||||
fragment: const ProxiesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.proxies,
|
||||
builder: (_) => ProviderScope(
|
||||
overrides: [
|
||||
queryProvider.overrideWith(
|
||||
() => Query(),
|
||||
),
|
||||
],
|
||||
child: const ProxiesView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.proxies,
|
||||
),
|
||||
),
|
||||
),
|
||||
modes: hasProxies
|
||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||
: [],
|
||||
),
|
||||
const NavigationItem(
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.folder),
|
||||
label: PageLabel.profiles,
|
||||
fragment: ProfilesFragment(
|
||||
builder: (_) => const ProfilesView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.profiles,
|
||||
),
|
||||
),
|
||||
),
|
||||
const NavigationItem(
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: PageLabel.requests,
|
||||
fragment: RequestsFragment(
|
||||
builder: (_) => const RequestsView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.requests,
|
||||
),
|
||||
),
|
||||
description: "requestsDesc",
|
||||
description: 'requestsDesc',
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
label: PageLabel.connections,
|
||||
fragment: ConnectionsFragment(
|
||||
builder: (_) => const ConnectionsView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.connections,
|
||||
),
|
||||
),
|
||||
description: "connectionsDesc",
|
||||
description: 'connectionsDesc',
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.storage),
|
||||
label: PageLabel.resources,
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
fragment: Resources(
|
||||
description: 'resourcesDesc',
|
||||
builder: (_) => const ResourcesView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.resources,
|
||||
),
|
||||
@@ -77,20 +85,20 @@ class Navigation {
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.adb),
|
||||
label: PageLabel.logs,
|
||||
fragment: const LogsFragment(
|
||||
builder: (_) => const LogsView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.logs,
|
||||
),
|
||||
),
|
||||
description: "logsDesc",
|
||||
description: 'logsDesc',
|
||||
modes: openLogs
|
||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||
: [],
|
||||
),
|
||||
const NavigationItem(
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.construction),
|
||||
label: PageLabel.tools,
|
||||
fragment: ToolsFragment(
|
||||
builder: (_) => const ToolsView(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.tools,
|
||||
),
|
||||
|
||||
@@ -19,6 +19,21 @@ class BaseNavigator {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// static Future<T?> modal<T>(BuildContext context, Widget child) async {
|
||||
// if (globalState.appState.viewMode != ViewMode.mobile) {
|
||||
// return await globalState.showCommonDialog<T>(
|
||||
// child: CommonModal(
|
||||
// child: child,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// return await Navigator.of(context).push<T>(
|
||||
// CommonRoute(
|
||||
// builder: (context) => child,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
class CommonDesktopRoute<T> extends PageRoute<T> {
|
||||
@@ -218,8 +233,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
begin: const _CommonEdgeShadowDecoration(),
|
||||
end: _CommonEdgeShadowDecoration(
|
||||
<Color>[
|
||||
widget.context.colorScheme.inverseSurface
|
||||
.withValues(alpha: 0.02),
|
||||
widget.context.colorScheme.inverseSurface.withValues(alpha: 0.02),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NumExt on num {
|
||||
String fixed({decimals = 2}) {
|
||||
String fixed({int decimals = 2}) {
|
||||
String formatted = toStringAsFixed(decimals);
|
||||
if (formatted.contains('.')) {
|
||||
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
|
||||
@@ -12,10 +13,14 @@ extension NumExt on num {
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
double get ap {
|
||||
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
extension DoubleExt on double {
|
||||
moreOrEqual(double value) {
|
||||
bool moreOrEqual(double value) {
|
||||
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +46,7 @@ extension OffsetExt on Offset {
|
||||
}
|
||||
|
||||
extension RectExt on Rect {
|
||||
doRectIntersect(Rect rect) {
|
||||
bool doRectIntersect(Rect rect) {
|
||||
return left < rect.right &&
|
||||
right > rect.left &&
|
||||
top < rect.bottom &&
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'common.dart';
|
||||
|
||||
extension PackageInfoExtension on PackageInfo {
|
||||
String get ua => [
|
||||
"$appName/v$version",
|
||||
"clash-verge",
|
||||
"Platform/${Platform.operatingSystem}",
|
||||
].join(" ");
|
||||
'$appName/v$version',
|
||||
'clash-verge',
|
||||
'Platform/${Platform.operatingSystem}',
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'constant.dart';
|
||||
|
||||
class AppPath {
|
||||
static AppPath? _instance;
|
||||
Completer<Directory> dataDir = Completer();
|
||||
@@ -32,7 +31,7 @@ class AppPath {
|
||||
}
|
||||
|
||||
String get executableExtension {
|
||||
return Platform.isWindows ? ".exe" : "";
|
||||
return system.isWindows ? '.exe' : '';
|
||||
}
|
||||
|
||||
String get executableDirPath {
|
||||
@@ -41,11 +40,11 @@ class AppPath {
|
||||
}
|
||||
|
||||
String get corePath {
|
||||
return join(executableDirPath, "FlClashCore$executableExtension");
|
||||
return join(executableDirPath, 'FlClashCore$executableExtension');
|
||||
}
|
||||
|
||||
String get helperPath {
|
||||
return join(executableDirPath, "$appHelperService$executableExtension");
|
||||
return join(executableDirPath, '$appHelperService$executableExtension');
|
||||
}
|
||||
|
||||
Future<String> get downloadDirPath async {
|
||||
@@ -60,12 +59,12 @@ class AppPath {
|
||||
|
||||
Future<String> get lockFilePath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, "FlClash.lock");
|
||||
return join(directory.path, 'FlClash.lock');
|
||||
}
|
||||
|
||||
Future<String> get sharedPreferencesPath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, "shared_preferences.json");
|
||||
return join(directory.path, 'shared_preferences.json');
|
||||
}
|
||||
|
||||
Future<String> get profilesPath async {
|
||||
@@ -73,16 +72,33 @@ class AppPath {
|
||||
return join(directory.path, profilesDirectoryName);
|
||||
}
|
||||
|
||||
Future<String?> getProfilePath(String? id) async {
|
||||
if (id == null) return null;
|
||||
Future<String> getProfilePath(String id) async {
|
||||
final directory = await profilesPath;
|
||||
return join(directory, "$id.yaml");
|
||||
return join(directory, '$id.yaml');
|
||||
}
|
||||
|
||||
Future<String?> getProvidersPath(String? id) async {
|
||||
if (id == null) return null;
|
||||
Future<String> getProvidersDirPath(String id) async {
|
||||
final directory = await profilesPath;
|
||||
return join(directory, "providers", id);
|
||||
return join(
|
||||
directory,
|
||||
'providers',
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getProvidersFilePath(
|
||||
String id,
|
||||
String type,
|
||||
String url,
|
||||
) async {
|
||||
final directory = await profilesPath;
|
||||
return join(
|
||||
directory,
|
||||
'providers',
|
||||
id,
|
||||
type,
|
||||
url.toMd5(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> get tempPath async {
|
||||
|
||||
@@ -20,9 +20,9 @@ class Picker {
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
fileName: fileName,
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
bytes: Platform.isAndroid ? bytes : null,
|
||||
bytes: system.isAndroid ? bytes : null,
|
||||
);
|
||||
if (!Platform.isAndroid && path != null) {
|
||||
if (!system.isAndroid && path != null) {
|
||||
final file = await File(path).create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ class Preferences {
|
||||
false;
|
||||
}
|
||||
|
||||
clearClashConfig() async {
|
||||
Future<void> clearClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
preferences?.remove(clashConfigKey);
|
||||
}
|
||||
|
||||
clearPreferences() async {
|
||||
Future<void> clearPreferences() async {
|
||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
||||
sharedPreferencesIns?.clear();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -13,17 +12,14 @@ class CommonPrint {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
log(String? text) {
|
||||
final payload = "[FlClash] $text";
|
||||
void log(String? text) {
|
||||
final payload = '[APP] $text';
|
||||
debugPrint(payload);
|
||||
if (globalState.isService) {
|
||||
if (!globalState.isInit) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: payload,
|
||||
),
|
||||
Log.app(payload),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,21 @@ class Render {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
active() {
|
||||
void active() {
|
||||
resume();
|
||||
pause();
|
||||
}
|
||||
|
||||
pause() {
|
||||
void pause() {
|
||||
throttler.call(
|
||||
DebounceTag.renderPause,
|
||||
FunctionTag.renderPause,
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
resume() {
|
||||
throttler.cancel(DebounceTag.renderPause);
|
||||
void resume() {
|
||||
throttler.cancel(FunctionTag.renderPause);
|
||||
_resume();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class Render {
|
||||
_drawFrame = _dispatcher.onDrawFrame;
|
||||
_dispatcher.onBeginFrame = null;
|
||||
_dispatcher.onDrawFrame = null;
|
||||
commonPrint.log("pause");
|
||||
commonPrint.log('pause');
|
||||
}
|
||||
|
||||
void _resume() {
|
||||
@@ -50,8 +50,8 @@ class Render {
|
||||
_dispatcher.onBeginFrame = _beginFrame;
|
||||
_dispatcher.onDrawFrame = _drawFrame;
|
||||
_dispatcher.scheduleFrame();
|
||||
commonPrint.log("resume");
|
||||
commonPrint.log('resume');
|
||||
}
|
||||
}
|
||||
|
||||
final render = system.isDesktop ? Render() : null;
|
||||
final Render? render = system.isDesktop ? Render() : null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
@@ -18,7 +19,7 @@ class Request {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
headers: {
|
||||
"User-Agent": browserUa,
|
||||
'User-Agent': browserUa,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -43,6 +44,16 @@ class Request {
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response> getTextResponseForUrl(String url) async {
|
||||
final response = await _clashDio.get(
|
||||
url,
|
||||
options: Options(
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<MemoryImage?> getImage(String url) async {
|
||||
if (url.isEmpty) return null;
|
||||
final response = await _dio.get<Uint8List>(
|
||||
@@ -58,7 +69,7 @@ class Request {
|
||||
|
||||
Future<Map<String, dynamic>?> checkForUpdate() async {
|
||||
final response = await _dio.get(
|
||||
"https://api.github.com/repos/$repository/releases/latest",
|
||||
'https://api.github.com/repos/$repository/releases/latest',
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
@@ -68,56 +79,61 @@ class Request {
|
||||
final remoteVersion = data['tag_name'];
|
||||
final version = globalState.packageInfo.version;
|
||||
final hasUpdate =
|
||||
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
if (!hasUpdate) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||
'https://ipwho.is/': IpInfo.fromIpwhoIsJson,
|
||||
'https://api.ip.sb/geoip/': IpInfo.fromIpSbJson,
|
||||
'https://ipapi.co/json/': IpInfo.fromIpApiCoJson,
|
||||
'https://ipinfo.io/json/': IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
try {
|
||||
final response = await Dio()
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
Duration(
|
||||
seconds: 30,
|
||||
),
|
||||
);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
continue;
|
||||
}
|
||||
if (response.data == null) {
|
||||
continue;
|
||||
}
|
||||
return source.value(response.data!);
|
||||
} catch (e) {
|
||||
commonPrint.log("checkIp error ===> $e");
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
throw "cancelled";
|
||||
Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
|
||||
var failureCount = 0;
|
||||
final futures = _ipInfoSources.entries.map((source) async {
|
||||
final Completer<Result<IpInfo?>> completer = Completer();
|
||||
handleFailRes() {
|
||||
if (!completer.isCompleted && failureCount == _ipInfoSources.length) {
|
||||
completer.complete(Result.success(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
final future = Dio().get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
future.then((res) {
|
||||
if (res.statusCode == HttpStatus.ok && res.data != null) {
|
||||
completer.complete(Result.success(source.value(res.data!)));
|
||||
} else {
|
||||
failureCount++;
|
||||
handleFailRes();
|
||||
}
|
||||
}).catchError((e) {
|
||||
failureCount++;
|
||||
if (e == DioExceptionType.cancel) {
|
||||
completer.complete(Result.error('cancelled'));
|
||||
}
|
||||
handleFailRes();
|
||||
});
|
||||
return completer.future;
|
||||
});
|
||||
final res = await Future.any(futures);
|
||||
cancelToken?.cancel();
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool> pingHelper() async {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get(
|
||||
"http://$localhost:$helperPort/ping",
|
||||
'http://$localhost:$helperPort/ping',
|
||||
options: Options(
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
@@ -130,7 +146,7 @@ class Request {
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
return false;
|
||||
}
|
||||
return (response.data as String) == helperTag;
|
||||
return (response.data as String) == globalState.coreSHA256;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@@ -140,10 +156,10 @@ class Request {
|
||||
try {
|
||||
final response = await _dio
|
||||
.post(
|
||||
"http://$localhost:$helperPort/start",
|
||||
'http://$localhost:$helperPort/start',
|
||||
data: json.encode({
|
||||
"path": appPath.corePath,
|
||||
"arg": arg,
|
||||
'path': appPath.corePath,
|
||||
'arg': arg,
|
||||
}),
|
||||
options: Options(
|
||||
responseType: ResponseType.plain,
|
||||
@@ -168,7 +184,7 @@ class Request {
|
||||
try {
|
||||
final response = await _dio
|
||||
.post(
|
||||
"http://$localhost:$helperPort/stop",
|
||||
'http://$localhost:$helperPort/stop',
|
||||
options: Options(
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
|
||||
@@ -35,7 +35,7 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return CommonAutoHiddenScrollBar(
|
||||
return CommonScrollBar(
|
||||
controller: details.controller,
|
||||
child: child,
|
||||
);
|
||||
@@ -88,6 +88,54 @@ class NextClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
}
|
||||
}
|
||||
|
||||
// class CacheScrollPositionController extends ScrollController {
|
||||
// final String key;
|
||||
//
|
||||
// CacheScrollPositionController({
|
||||
// required this.key,
|
||||
// double initialScrollOffset = 0.0,
|
||||
// super.keepScrollOffset = true,
|
||||
// super.debugLabel,
|
||||
// super.onAttach,
|
||||
// super.onDetach,
|
||||
// });
|
||||
//
|
||||
// @override
|
||||
// ScrollPosition createScrollPosition(
|
||||
// ScrollPhysics physics,
|
||||
// ScrollContext context,
|
||||
// ScrollPosition? oldPosition,
|
||||
// ) {
|
||||
// return ScrollPositionWithSingleContext(
|
||||
// physics: physics,
|
||||
// context: context,
|
||||
// initialPixels:
|
||||
// globalState.scrollPositionCache[key] ?? initialScrollOffset,
|
||||
// keepScrollOffset: keepScrollOffset,
|
||||
// oldPosition: oldPosition,
|
||||
// debugLabel: debugLabel,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// double? get cacheOffset => globalState.scrollPositionCache[key];
|
||||
//
|
||||
// _handleScroll() {
|
||||
// globalState.scrollPositionCache[key] = position.pixels;
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void attach(ScrollPosition position) {
|
||||
// super.attach(position);
|
||||
// addListener(_handleScroll);
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void detach(ScrollPosition position) {
|
||||
// removeListener(_handleScroll);
|
||||
// super.detach(position);
|
||||
// }
|
||||
// }
|
||||
|
||||
class ReverseScrollController extends ScrollController {
|
||||
ReverseScrollController({
|
||||
super.initialScrollOffset,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
import 'print.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
@@ -8,6 +10,13 @@ extension StringExtension on String {
|
||||
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
|
||||
}
|
||||
|
||||
dynamic get splitByMultipleSeparators {
|
||||
final parts =
|
||||
split(RegExp(r'[, ;]+')).where((part) => part.isNotEmpty).toList();
|
||||
|
||||
return parts.length > 1 ? parts : this;
|
||||
}
|
||||
|
||||
int compareToLower(String other) {
|
||||
return toLowerCase().compareTo(
|
||||
other.toLowerCase(),
|
||||
@@ -38,6 +47,10 @@ extension StringExtension on String {
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSvg {
|
||||
return endsWith('.svg');
|
||||
}
|
||||
|
||||
bool get isRegex {
|
||||
try {
|
||||
RegExp(this);
|
||||
@@ -47,6 +60,15 @@ extension StringExtension on String {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String toMd5() {
|
||||
final bytes = utf8.encode(this);
|
||||
return md5.convert(bytes).toString();
|
||||
}
|
||||
|
||||
// bool containsToLower(String target) {
|
||||
// return toLowerCase().contains(target);
|
||||
// }
|
||||
}
|
||||
|
||||
extension StringExtensionSafe on String? {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:ffi/ffi.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 'package:path/path.dart';
|
||||
|
||||
class System {
|
||||
static System? _instance;
|
||||
@@ -18,25 +21,32 @@ class System {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool get isDesktop =>
|
||||
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
bool get isDesktop => isWindows || isMacOS || isLinux;
|
||||
|
||||
bool get isWindows => Platform.isWindows;
|
||||
|
||||
bool get isMacOS => Platform.isMacOS;
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
bool get isLinux => Platform.isLinux;
|
||||
|
||||
Future<int> get version async {
|
||||
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
|
||||
return switch (Platform.operatingSystem) {
|
||||
"macos" => (deviceInfo as MacOsDeviceInfo).majorVersion,
|
||||
"android" => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
|
||||
"windows" => (deviceInfo as WindowsDeviceInfo).majorVersion,
|
||||
'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion,
|
||||
'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
|
||||
'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion,
|
||||
String() => 0
|
||||
};
|
||||
}
|
||||
|
||||
Future<bool> checkIsAdmin() async {
|
||||
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
|
||||
if (Platform.isWindows) {
|
||||
if (system.isWindows) {
|
||||
final result = await windows?.checkService();
|
||||
return result == WindowsHelperServiceStatus.running;
|
||||
} else if (Platform.isMacOS) {
|
||||
} else if (system.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')) {
|
||||
@@ -55,24 +65,30 @@ class System {
|
||||
}
|
||||
|
||||
Future<AuthorizeCode> authorizeCore() async {
|
||||
if (system.isAndroid) {
|
||||
return AuthorizeCode.error;
|
||||
}
|
||||
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
|
||||
final isAdmin = await checkIsAdmin();
|
||||
if (isAdmin) {
|
||||
return AuthorizeCode.none;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
|
||||
if (system.isWindows) {
|
||||
final result = await windows?.registerService();
|
||||
if (result == true) {
|
||||
return AuthorizeCode.success;
|
||||
}
|
||||
return AuthorizeCode.error;
|
||||
} else if (Platform.isMacOS) {
|
||||
}
|
||||
|
||||
if (system.isMacOS) {
|
||||
final shell = 'chown root:admin $corePath; chmod +sx $corePath';
|
||||
final arguments = [
|
||||
"-e",
|
||||
'-e',
|
||||
'do shell script "$shell" with administrator privileges',
|
||||
];
|
||||
final result = await Process.run("osascript", arguments);
|
||||
final result = await Process.run('osascript', arguments);
|
||||
if (result.exitCode != 0) {
|
||||
return AuthorizeCode.error;
|
||||
}
|
||||
@@ -86,7 +102,7 @@ class System {
|
||||
),
|
||||
);
|
||||
final arguments = [
|
||||
"-c",
|
||||
'-c',
|
||||
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
|
||||
];
|
||||
final result = await Process.run(shell, arguments);
|
||||
@@ -98,13 +114,13 @@ class System {
|
||||
return AuthorizeCode.error;
|
||||
}
|
||||
|
||||
back() async {
|
||||
Future<void> back() async {
|
||||
await app?.moveTaskToBack();
|
||||
await window?.hide();
|
||||
}
|
||||
|
||||
exit() async {
|
||||
if (Platform.isAndroid) {
|
||||
Future<void> exit() async {
|
||||
if (system.isAndroid) {
|
||||
await SystemNavigator.pop();
|
||||
}
|
||||
await window?.close();
|
||||
@@ -112,3 +128,287 @@ class System {
|
||||
}
|
||||
|
||||
final system = System();
|
||||
|
||||
class Windows {
|
||||
static Windows? _instance;
|
||||
late DynamicLibrary _shell32;
|
||||
|
||||
Windows._internal() {
|
||||
_shell32 = DynamicLibrary.open('shell32.dll');
|
||||
}
|
||||
|
||||
factory Windows() {
|
||||
_instance ??= Windows._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool runas(String command, String arguments) {
|
||||
final commandPtr = command.toNativeUtf16();
|
||||
final argumentsPtr = arguments.toNativeUtf16();
|
||||
final operationPtr = 'runas'.toNativeUtf16();
|
||||
|
||||
final shellExecute = _shell32.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
Int32 nShowCmd),
|
||||
int Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
int nShowCmd)>('ShellExecuteW');
|
||||
|
||||
final result = shellExecute(
|
||||
nullptr,
|
||||
operationPtr,
|
||||
commandPtr,
|
||||
argumentsPtr,
|
||||
nullptr,
|
||||
1,
|
||||
);
|
||||
|
||||
calloc.free(commandPtr);
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
commonPrint.log('windows runas: $command $arguments resultCode:$result');
|
||||
|
||||
if (result < 42) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _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"?>
|
||||
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<LogonType>InteractiveToken</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Triggers>
|
||||
<LogonTrigger/>
|
||||
</Triggers>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>false</AllowHardTerminate>
|
||||
<StartWhenAvailable>false</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>"${Platform.resolvedExecutable}"</Command>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>''';
|
||||
final taskPath = join(await appPath.tempPath, 'task.xml');
|
||||
await File(taskPath).create(recursive: true);
|
||||
await File(taskPath)
|
||||
.writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
|
||||
final commandLine = [
|
||||
'/Create',
|
||||
'/TN',
|
||||
appName,
|
||||
'/XML',
|
||||
'%s',
|
||||
'/F',
|
||||
].join(' ');
|
||||
return runas(
|
||||
'schtasks',
|
||||
commandLine.replaceFirst('%s', taskPath),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final windows = system.isWindows ? Windows() : null;
|
||||
|
||||
class MacOS {
|
||||
static MacOS? _instance;
|
||||
|
||||
List<String>? originDns;
|
||||
|
||||
MacOS._internal();
|
||||
|
||||
factory MacOS() {
|
||||
_instance ??= MacOS._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<String?> get defaultServiceName async {
|
||||
final result = await Process.run('route', ['-n', 'get', 'default']);
|
||||
final output = result.stdout.toString();
|
||||
final deviceLine = output
|
||||
.split('\n')
|
||||
.firstWhere((s) => s.contains('interface:'), orElse: () => '');
|
||||
final lineSplits = deviceLine.trim().split(' ');
|
||||
if (lineSplits.length != 2) {
|
||||
return null;
|
||||
}
|
||||
final device = lineSplits[1];
|
||||
final serviceResult = await Process.run(
|
||||
'networksetup',
|
||||
['-listnetworkserviceorder'],
|
||||
);
|
||||
final serviceResultOutput = serviceResult.stdout.toString();
|
||||
final currentService = serviceResultOutput.split('\n\n').firstWhere(
|
||||
(s) => s.contains('Device: $device'),
|
||||
orElse: () => '',
|
||||
);
|
||||
if (currentService.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final currentServiceNameLine = currentService.split('\n').firstWhere(
|
||||
(line) => RegExp(r'^\(\d+\).*').hasMatch(line),
|
||||
orElse: () => '');
|
||||
final currentServiceNameLineSplits =
|
||||
currentServiceNameLine.trim().split(' ');
|
||||
if (currentServiceNameLineSplits.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return currentServiceNameLineSplits[1];
|
||||
}
|
||||
|
||||
Future<List<String>?> get systemDns async {
|
||||
final deviceServiceName = await defaultServiceName;
|
||||
if (deviceServiceName == null) {
|
||||
return null;
|
||||
}
|
||||
final result = await Process.run(
|
||||
'networksetup',
|
||||
['-getdnsservers', deviceServiceName],
|
||||
);
|
||||
final output = result.stdout.toString().trim();
|
||||
if (output.startsWith("There aren't any DNS Servers set on")) {
|
||||
originDns = [];
|
||||
} else {
|
||||
originDns = output.split('\n');
|
||||
}
|
||||
return originDns;
|
||||
}
|
||||
|
||||
Future<void> updateDns(bool restore) async {
|
||||
final serviceName = await defaultServiceName;
|
||||
if (serviceName == null) {
|
||||
return;
|
||||
}
|
||||
List<String>? nextDns;
|
||||
if (restore) {
|
||||
nextDns = originDns;
|
||||
} else {
|
||||
final originDns = await systemDns;
|
||||
if (originDns == null) {
|
||||
return;
|
||||
}
|
||||
final needAddDns = '223.5.5.5';
|
||||
if (originDns.contains(needAddDns)) {
|
||||
return;
|
||||
}
|
||||
nextDns = List.from(originDns)..add(needAddDns);
|
||||
}
|
||||
if (nextDns == null) {
|
||||
return;
|
||||
}
|
||||
await Process.run(
|
||||
'networksetup',
|
||||
[
|
||||
'-setdnsservers',
|
||||
serviceName,
|
||||
if (nextDns.isNotEmpty) ...nextDns,
|
||||
if (nextDns.isEmpty) 'Empty',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final macOS = system.isMacOS ? MacOS() : null;
|
||||
|
||||
@@ -4,36 +4,43 @@ import 'package:flutter/material.dart';
|
||||
class CommonTheme {
|
||||
final BuildContext context;
|
||||
final Map<String, Color> _colorMap;
|
||||
final double textScaleFactor;
|
||||
|
||||
CommonTheme.of(this.context) : _colorMap = {};
|
||||
CommonTheme.of(
|
||||
this.context,
|
||||
this.textScaleFactor,
|
||||
) : _colorMap = {};
|
||||
|
||||
Color get darkenSecondaryContainer {
|
||||
return _colorMap.getCacheValue(
|
||||
"darkenSecondaryContainer",
|
||||
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1),
|
||||
return _colorMap.updateCacheValue(
|
||||
'darkenSecondaryContainer',
|
||||
() => context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1),
|
||||
);
|
||||
}
|
||||
|
||||
Color get darkenSecondaryContainerLighter {
|
||||
return _colorMap.getCacheValue(
|
||||
"darkenSecondaryContainerLighter",
|
||||
context.colorScheme.secondaryContainer
|
||||
return _colorMap.updateCacheValue(
|
||||
'darkenSecondaryContainerLighter',
|
||||
() => context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1)
|
||||
.opacity60,
|
||||
);
|
||||
}
|
||||
|
||||
Color get darken2SecondaryContainer {
|
||||
return _colorMap.getCacheValue(
|
||||
"darken2SecondaryContainer",
|
||||
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2),
|
||||
return _colorMap.updateCacheValue(
|
||||
'darken2SecondaryContainer',
|
||||
() => context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.2),
|
||||
);
|
||||
}
|
||||
|
||||
Color get darken3PrimaryContainer {
|
||||
return _colorMap.getCacheValue(
|
||||
"darken3PrimaryContainer",
|
||||
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3),
|
||||
return _colorMap.updateCacheValue(
|
||||
'darken3PrimaryContainer',
|
||||
() => context.colorScheme.primaryContainer
|
||||
.blendDarken(context, factor: 0.3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/utils.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -10,7 +11,7 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
import 'app_localizations.dart';
|
||||
import 'constant.dart';
|
||||
import 'other.dart';
|
||||
import 'system.dart';
|
||||
import 'window.dart';
|
||||
|
||||
class Tray {
|
||||
@@ -18,14 +19,14 @@ class Tray {
|
||||
required Brightness? brightness,
|
||||
bool force = false,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
if (system.isAndroid) {
|
||||
return;
|
||||
}
|
||||
if (Platform.isLinux || force) {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
await trayManager.setIcon(
|
||||
other.getTrayIconPath(
|
||||
utils.getTrayIconPath(
|
||||
brightness: brightness ??
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
),
|
||||
@@ -38,11 +39,11 @@ class Tray {
|
||||
}
|
||||
}
|
||||
|
||||
update({
|
||||
Future<void> update({
|
||||
required TrayState trayState,
|
||||
bool focus = false,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
if (system.isAndroid) {
|
||||
return;
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
@@ -80,7 +81,7 @@ class Tray {
|
||||
);
|
||||
}
|
||||
menuItems.add(MenuItem.separator());
|
||||
if (!Platform.isWindows) {
|
||||
if (system.isMacOS) {
|
||||
for (final group in trayState.groups) {
|
||||
List<MenuItem> subMenuItems = [];
|
||||
for (final proxy in group.all) {
|
||||
@@ -169,8 +170,8 @@ class Tray {
|
||||
}
|
||||
}
|
||||
|
||||
updateTrayTitle([Traffic? traffic]) async {
|
||||
// if (!Platform.isMacOS) {
|
||||
Future<void> updateTrayTitle([Traffic? traffic]) async {
|
||||
// if (!system.isMacOS) {
|
||||
// return;
|
||||
// }
|
||||
// if (traffic == null) {
|
||||
@@ -183,11 +184,10 @@ class Tray {
|
||||
}
|
||||
|
||||
Future<void> _copyEnv(int port) async {
|
||||
final url = "http://127.0.0.1:$port";
|
||||
final url = 'http://127.0.0.1:$port';
|
||||
|
||||
final cmdline = Platform.isWindows
|
||||
? "set \$env:all_proxy=$url"
|
||||
: "export all_proxy=$url";
|
||||
final cmdline =
|
||||
system.isWindows ? 'set \$env:all_proxy=$url' : 'export all_proxy=$url';
|
||||
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user