Compare commits

..

51 Commits

Author SHA1 Message Date
chen08209
9100962939 cache 2026-03-25 19:10:08 +08:00
chen08209
2dbee3a22d cache 2026-03-23 19:25:17 +08:00
chen08209
1791ead92d cache 2026-03-23 13:58:51 +08:00
chen08209
71903feeeb cache 2026-03-23 10:24:45 +08:00
chen08209
7e234dc62d cache 2026-03-23 10:13:30 +08:00
chen08209
5d3122f5f4 cache 2026-03-22 15:16:12 +08:00
chen08209
d30eb6a7c6 cache 2026-03-20 21:52:06 +08:00
chen08209
b8dd8ada2e cache 2026-03-20 21:40:29 +08:00
chen08209
9e04b004ba cache 2026-03-20 21:20:35 +08:00
chen08209
c8a93029aa cache 2026-03-20 19:00:53 +08:00
chen08209
490e1bae87 cache 2026-03-20 18:55:29 +08:00
chen08209
ce3093c80b cache 2026-03-19 18:19:37 +08:00
chen08209
2023cecdfc cache 2026-03-19 17:42:41 +08:00
chen08209
b20efc32a5 cache 2026-03-19 15:30:39 +08:00
chen08209
b3123eb35d cache 2026-03-19 15:11:47 +08:00
chen08209
4be3a483fa cache 2026-03-17 16:17:39 +08:00
chen08209
e5c18d97cf cache 2026-03-15 01:10:43 +08:00
chen08209
b41ca491ae cache 2026-03-14 19:55:50 +08:00
chen08209
eabae2aa29 cache 2026-03-12 16:26:30 +08:00
chen08209
f846a97aa3 cache 2026-03-12 15:24:09 +08:00
chen08209
88d3c23cdc cache 2026-03-12 15:04:33 +08:00
chen08209
67402899a8 cache 2026-03-11 17:45:20 +08:00
chen08209
bc0bbc6ede cache 2026-03-11 17:19:32 +08:00
chen08209
f0417cac58 cache 2026-03-11 16:46:26 +08:00
chen08209
7b46347016 cache 2026-03-11 10:24:38 +08:00
chen08209
3f5e7b80bc cache 2026-03-10 19:24:23 +08:00
chen08209
8296302211 cache 2026-03-10 14:25:00 +08:00
chen08209
e200a89796 cache 2026-03-09 14:15:30 +08:00
chen08209
bc552b63bb cache 2026-03-05 17:29:05 +08:00
chen08209
50e0ae721e cache 2026-03-04 18:47:56 +08:00
chen08209
b61d985657 cache 2026-03-03 16:23:33 +08:00
chen08209
09ca576752 cache 2026-03-03 10:12:29 +08:00
chen08209
37f635a16a cache 2026-03-02 18:59:31 +08:00
chen08209
08376b7ae1 cache 2026-02-28 10:39:45 +08:00
chen08209
dfc59aac4b cache 2026-02-27 16:15:21 +08:00
chen08209
f94a1491bf cache 2026-02-14 22:00:05 +08:00
chen08209
0602146d50 cache 2026-02-12 22:28:48 +08:00
chen08209
b5e5d9c632 cache 2026-02-11 16:49:02 +08:00
chen08209
b742ab76bc cache 2026-02-06 15:38:01 +08:00
chen08209
c46168eff8 cache 2026-02-05 16:21:14 +08:00
chen08209
fa28b0d610 cache 2026-02-04 15:52:44 +08:00
chen08209
23b349c3df cache 2026-02-03 18:44:05 +08:00
chen08209
cb948446be cache 2026-02-02 10:15:11 +08:00
chen08209
db49cd81ce Add sqlite store
Optimize android quick action

Optimize backup and restore

Optimize more details
2026-02-02 10:15:11 +08:00
chen08209
6e404ab19c Fix windows some issues
Optimize overwrite handle

Optimize access control page

Optimize some details
2025-12-12 14:33:03 +08:00
chen08209
2395a4b20c Update changelog 2025-10-08 08:41:35 +00:00
chen08209
d3c3f04062 Fix android tile service
Support append system DNS

Fix some issues
2025-10-08 16:28:02 +08:00
chen08209
201062dc5d Update changelog 2025-09-27 07:31:55 +00:00
chen08209
45b163184d Fix some issues
Optimize Windows service mode

Update core
2025-09-27 15:17:19 +08:00
chen08209
2ab70f193a Update changelog 2025-09-23 07:44:50 +00:00
chen08209
ed7868282a Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-23 15:23:58 +08:00
297 changed files with 30625 additions and 14540 deletions

View File

@@ -22,14 +22,14 @@ jobs:
os: ubuntu-22.04 os: ubuntu-22.04
arch: amd64 arch: amd64
- platform: macos - platform: macos
os: macos-13 os: macos-15-intel
arch: amd64 arch: amd64
- platform: macos - platform: macos
os: macos-latest os: macos-latest
arch: arm64 arch: arm64
- platform: windows # - platform: windows
os: windows-11-arm # os: windows-11-arm
arch: arm64 # arch: arm64
- platform: linux - platform: linux
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
arch: arm64 arch: arm64
@@ -64,22 +64,25 @@ jobs:
cache-dependency-path: | cache-dependency-path: |
core/go.sum core/go.sum
- 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: 'master'
cache: true
- name: Setup Flutter - name: Setup Flutter
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }} if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: stable
flutter-version: 3.35.7
cache: true
- name: Setup Flutter With Other
if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')
uses: subosito/flutter-action@v2
with:
channel: master
flutter-version: 3.35.7
cache: true cache: true
# flutter-version: 3.29.3
- name: Get Flutter Dependency - name: Get Flutter Dependency
run: flutter pub get run: |
flutter --version
flutter pub get
- name: Setup - name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }} run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
@@ -104,34 +107,26 @@ jobs:
- name: Generate - name: Generate
if: ${{ env.IS_STABLE == 'true' }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) last_ver=$(grep -m1 '^## ' CHANGELOG.md 2>/dev/null | sed 's/^## //')
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag="" tags=($(git tag --merged HEAD --sort=-creatordate))
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then temp="NEW_CHANGELOG.md" > "$temp"
tag=${tags[$i]}
else for i in "${!tags[@]}"; do
tag="" curr="${tags[i]}"
fi [[ "$curr" == "$last_ver" ]] && break
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then prev="${tags[i+1]}"
break range="${prev:+$prev..}$curr"
fi
fi echo -e "## $curr\n" >> "$temp"
if [ -n "$currentTag" ]; then git log --no-merges --pretty=format:"%B" "$range" | \
echo "## $currentTag" >> NEW_CHANGELOG.md awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$temp"
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done done
cat CHANGELOG.md >> NEW_CHANGELOG.md [ -f CHANGELOG.md ] && cat CHANGELOG.md >> "$temp"
cat NEW_CHANGELOG.md > CHANGELOG.md
mv "$temp" CHANGELOG.md
- name: Commit - name: Commit
if: ${{ env.IS_STABLE == 'true' }} if: ${{ env.IS_STABLE == 'true' }}
@@ -181,31 +176,24 @@ jobs:
- name: Generate release.md - name: Generate release.md
run: | run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) tags=($(git tag --merged HEAD --sort=-creatordate))
preTag=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "") preTag=$(curl -s "https://api.github.com/repos/chen08209/FlClash/releases/latest" | \
currentTag="" sed -nE 's/.*"tag_name": "([^"]+)".*/\1/p')
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then [ -z "$preTag" ] && preTag=""
tag=${tags[$i]}
else out="release.md" > "$out"
tag=""
fi for i in "${!tags[@]}"; do
if [ -n "$currentTag" ]; then curr="${tags[i]}"
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then [[ "$curr" == "$preTag" ]] && break
break
fi prev="${tags[i+1]}"
fi range="${prev:+$prev..}$curr"
if [ -n "$currentTag" ]; then
if [ -n "$tag" ]; then git log --no-merges --pretty=format:"%B" "$range" | \
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$out"
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
fi
echo "" >> release.md
fi
currentTag=$tag
done done
- name: Push to telegram - name: Push to telegram
env: env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}

27
.gitignore vendored
View File

@@ -21,7 +21,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
# is commented out by default. # is commented out by default.
#.vscode/ .vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
**/doc/api/ **/doc/api/
@@ -41,15 +41,30 @@ app.*.symbols
# Obfuscation related # Obfuscation related
app.*.map.json app.*.map.json
#AI generated
CLAUDE.md
/.claude
# Android Studio will place build artifacts here # Android Studio will place build artifacts here
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
/android/**/.cxx
/android/**/build
/android/common/**/.**/
/android/common/local.*
/android/core/**/includes/
/android/core/**/cmake-build-*/
/android/core/**/jniLibs/
#FlClash
#libclash
/libclash/ /libclash/
#jniLibs
/android/app/src/main/jniLibs/ /android/app/src/main/jniLibs/
/services/helper/target
/macos/**/Package.resolved
devtools_options.yaml
# FVM Version Cache
.fvm/
.fvmrc

4
.gitmodules vendored
View File

@@ -6,5 +6,9 @@
path = plugins/flutter_distributor path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash branch = FlClash
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main

View File

@@ -1,11 +1,11 @@
# This file tracks properties of this Flutter project. # This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc. # Used by Flutter tool to assess capabilities and perform upgrades etc.
# #
# This file should be version controlled. # This file should be version controlled and should not be manually edited.
version: version:
revision: 796c8ef79279f9c774545b3771238c3098dbefab revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: stable channel: "stable"
project_type: app project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: android
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: ios
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: linux
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: macos
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: web
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: windows - platform: windows
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section # User provided section

7
.run/main.dart.run.xml Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="additionalArgs" value="--dart-define-from-file env.json" />
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,3 +1,39 @@
## v0.8.90
- Fix android tile service
- Support append system DNS
- Fix some issues
- Update changelog
## v0.8.89
- Fix some issues
- Optimize Windows service mode
- Update core
- Update changelog
## v0.8.88
- Add android separates the core process
- Support core status check and force restart
- Optimize proxies page and access page
- Update flutter and pub dependencies
- Update go version
- Optimize more details
- Update changelog
## v0.8.87 ## v0.8.87
- Optimize desktop view - Optimize desktop view

View File

@@ -1,10 +0,0 @@
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:
dart ./setup.dart macos --arch arm64 --out core

View File

@@ -64,16 +64,17 @@ android {
buildTypes { buildTypes {
debug { debug {
isMinifyEnabled = false isMinifyEnabled = false
applicationIdSuffix = ".debug" applicationIdSuffix = ".dev"
} }
release { release {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
signingConfig = if (isRelease) { if (isRelease) {
signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
} else { } else {
signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
applicationIdSuffix = ".dev"
} }
proguardFiles( proguardFiles(

View File

@@ -41,6 +41,25 @@
"other_platform_oauth_client": [] "other_platform_oauth_client": []
} }
} }
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash.dev"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
} }
] ]
} }

View File

@@ -22,6 +22,7 @@
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
<application <application
android:name=".Application" android:name=".Application"

View File

@@ -20,7 +20,9 @@ class BroadcastReceiver : BroadcastReceiver() {
BroadcastAction.SERVICE_DESTROYED.action -> { BroadcastAction.SERVICE_DESTROYED.action -> {
GlobalState.log("Receiver service destroyed") GlobalState.log("Receiver service destroyed")
State.handleStopServiceAction() GlobalState.launch {
State.handleStopServiceAction()
}
} }
} }
} }

View File

@@ -1,13 +1,18 @@
package com.follow.clash package com.follow.clash
import android.app.Application
import android.content.Context.MODE_PRIVATE
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.widget.Toast
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.common.GlobalState import com.follow.clash.common.GlobalState
import com.follow.clash.models.SharedState
import com.google.gson.Gson
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@@ -21,6 +26,30 @@ import kotlin.coroutines.resume
private const val ICON_TTL_DAYS = 1L private const val ICON_TTL_DAYS = 1L
val Application.sharedState: SharedState
get() {
try {
val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
val res = sp.getString("flutter.sharedState", "")
return Gson().fromJson(res, SharedState::class.java)
} catch (_: Exception) {
return SharedState()
}
}
private var lastToast: Toast? = null
fun Application.showToast(text: String?) {
Handler(Looper.getMainLooper()).post {
lastToast?.cancel()
lastToast = Toast.makeText(this, text, Toast.LENGTH_LONG).apply {
show()
}
}
}
suspend fun PackageManager.getPackageIconPath(packageName: String): String = suspend fun PackageManager.getPackageIconPath(packageName: String): String =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val cacheDir = GlobalState.application.cacheDir val cacheDir = GlobalState.application.cacheDir
@@ -118,4 +147,4 @@ fun <T> MethodChannel.invokeMethodOnMainThread(
} }
}) })
} }
} }

View File

@@ -1,7 +1,6 @@
package com.follow.clash package com.follow.clash
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.follow.clash.common.GlobalState import com.follow.clash.common.GlobalState
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.ServicePlugin
@@ -18,9 +17,6 @@ class MainActivity : FlutterActivity(),
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch {
State.destroyServiceEngine()
}
} }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {

View File

@@ -1,12 +1,15 @@
package com.follow.clash package com.follow.clash
import com.follow.clash.common.GlobalState
import com.follow.clash.common.ServiceDelegate import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.formatString import com.follow.clash.common.formatString
import com.follow.clash.common.intent import com.follow.clash.common.intent
import com.follow.clash.service.IAckInterface
import com.follow.clash.service.ICallbackInterface import com.follow.clash.service.ICallbackInterface
import com.follow.clash.service.IEventInterface import com.follow.clash.service.IEventInterface
import com.follow.clash.service.IRemoteInterface import com.follow.clash.service.IRemoteInterface
import com.follow.clash.service.IResultInterface import com.follow.clash.service.IResultInterface
import com.follow.clash.service.IVoidInterface
import com.follow.clash.service.RemoteService import com.follow.clash.service.RemoteService
import com.follow.clash.service.models.NotificationParams import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions import com.follow.clash.service.models.VpnOptions
@@ -39,21 +42,61 @@ object Service {
delegate.unbind() delegate.unbind()
} }
suspend fun invokeAction(data: String, cb: (result: String) -> Unit): Result<Unit> { suspend fun invokeAction(data: String, cb: ((result: String) -> Unit)?): Result<Unit> {
val res = mutableListOf<ByteArray>() val res = mutableListOf<ByteArray>()
return delegate.useService { return delegate.useService {
it.invokeAction( it.invokeAction(
data, object : ICallbackInterface.Stub() { data, object : ICallbackInterface.Stub() {
override fun onResult(result: ByteArray?, isSuccess: Boolean) { override fun onResult(
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
res.add(result ?: byteArrayOf()) res.add(result ?: byteArrayOf())
ack?.onAck()
if (isSuccess) { if (isSuccess) {
cb(res.formatString()) cb?.let { cb ->
cb(res.formatString())
}
} }
} }
}) })
} }
} }
suspend fun quickSetup(
initParamsString: String,
setupParamsString: String,
onStarted: (() -> Unit)?,
onResult: ((result: String) -> Unit)?,
): Result<Unit> {
val res = mutableListOf<ByteArray>()
return delegate.useService {
it.quickSetup(
initParamsString,
setupParamsString,
object : ICallbackInterface.Stub() {
override fun onResult(
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
res.add(result ?: byteArrayOf())
ack?.onAck()
if (isSuccess) {
onResult?.let { cb ->
cb(res.formatString())
}
}
}
},
object : IVoidInterface.Stub() {
override fun invoke() {
onStarted?.let { onStarted ->
onStarted()
}
}
}
)
}
}
suspend fun setEventListener( suspend fun setEventListener(
cb: ((result: String?) -> Unit)? cb: ((result: String?) -> Unit)?
): Result<Unit> { ): Result<Unit> {
@@ -63,12 +106,13 @@ object Service {
when (cb != null) { when (cb != null) {
true -> object : IEventInterface.Stub() { true -> object : IEventInterface.Stub() {
override fun onEvent( override fun onEvent(
id: String, data: ByteArray?, isSuccess: Boolean id: String, data: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) { ) {
if (results[id] == null) { if (results[id] == null) {
results[id] = mutableListOf() results[id] = mutableListOf()
} }
results[id]?.add(data ?: byteArrayOf()) results[id]?.add(data ?: byteArrayOf())
ack?.onAck()
if (isSuccess) { if (isSuccess) {
cb(results[id]?.formatString()) cb(results[id]?.formatString())
results.remove(id) results.remove(id)
@@ -77,8 +121,7 @@ object Service {
} }
false -> null false -> null
} })
)
} }
} }
@@ -112,6 +155,7 @@ object Service {
try { try {
block(callback) block(callback)
} catch (e: Exception) { } catch (e: Exception) {
GlobalState.log("awaitIResultInterface $e")
if (continuation.isActive) { if (continuation.isActive) {
continuation.resumeWithException(e) continuation.resumeWithException(e)
} }

View File

@@ -1,18 +1,17 @@
package com.follow.clash package com.follow.clash
import android.net.VpnService
import com.follow.clash.common.GlobalState import com.follow.clash.common.GlobalState
import com.follow.clash.models.SharedState
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector import com.follow.clash.service.models.NotificationParams
import com.google.gson.Gson
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
enum class RunState { enum class RunState {
START, PENDING, STOP START, PENDING, STOP
@@ -25,20 +24,17 @@ object State {
var runTime: Long = 0 var runTime: Long = 0
var sharedState: SharedState = SharedState()
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP) val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
var flutterEngine: FlutterEngine? = null var flutterEngine: FlutterEngine? = null
var serviceFlutterEngine: FlutterEngine? = null
val appPlugin: AppPlugin? val appPlugin: AppPlugin?
get() = flutterEngine?.plugin<AppPlugin>() ?: serviceFlutterEngine?.plugin<AppPlugin>() get() = flutterEngine?.plugin<AppPlugin>()
val servicePlugin: ServicePlugin?
get() = flutterEngine?.plugin<ServicePlugin>()
?: serviceFlutterEngine?.plugin<ServicePlugin>()
val tilePlugin: TilePlugin? val tilePlugin: TilePlugin?
get() = flutterEngine?.plugin<TilePlugin>() ?: serviceFlutterEngine?.plugin<TilePlugin>() get() = flutterEngine?.plugin<TilePlugin>()
suspend fun handleToggleAction() { suspend fun handleToggleAction() {
var action: (suspend () -> Unit)? var action: (suspend () -> Unit)?
@@ -54,35 +50,52 @@ object State {
suspend fun handleSyncState() { suspend fun handleSyncState() {
runLock.withLock { runLock.withLock {
Service.bind() try {
runTime = Service.getRunTime() Service.bind()
val runState = when (runTime == 0L) { runTime = Service.getRunTime()
true -> RunState.STOP val runState = when (runTime == 0L) {
false -> RunState.START true -> RunState.STOP
false -> RunState.START
}
runStateFlow.tryEmit(runState)
} catch (_: Exception) {
runStateFlow.tryEmit(RunState.STOP)
} }
runStateFlow.tryEmit(runState)
} }
} }
suspend fun handleStartServiceAction() { suspend fun handleStartServiceAction() {
tilePlugin?.handleStart() runLock.withLock {
if (flutterEngine != null) { if (runStateFlow.value != RunState.STOP) {
return return
}
tilePlugin?.handleStart()
if (flutterEngine != null) {
return
}
startServiceWithPref()
} }
startServiceWithEngine()
} }
fun handleStopServiceAction() { suspend fun handleStopServiceAction() {
tilePlugin?.handleStop() runLock.withLock {
if (flutterEngine != null || serviceFlutterEngine != null) { if (runStateFlow.value != RunState.START) {
return return
}
tilePlugin?.handleStop()
if (flutterEngine != null) {
return
}
GlobalState.application.showToast(sharedState.stopTip)
handleStopService()
} }
handleStopService()
} }
fun handleStartService() { fun handleStartService() {
val appPlugin = flutterEngine?.plugin<AppPlugin>()
if (appPlugin != null) { if (appPlugin != null) {
appPlugin?.requestNotificationsPermission { appPlugin.requestNotificationsPermission {
startService() startService()
} }
return return
@@ -90,69 +103,100 @@ object State {
startService() startService()
} }
suspend fun destroyServiceEngine() { private fun startServiceWithPref() {
runLock.withLock { GlobalState.launch {
withContext(Dispatchers.Main) { runLock.withLock {
runCatching { if (runStateFlow.value != RunState.STOP) {
serviceFlutterEngine?.destroy() return@launch
serviceFlutterEngine = null
} }
sharedState = GlobalState.application.sharedState
setupAndStart()
} }
} }
} }
suspend fun startServiceWithEngine() { suspend fun syncState() {
runLock.withLock { GlobalState.setCrashlytics(sharedState.crashlytics)
if (serviceFlutterEngine != null || runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) { Service.updateNotificationParams(
return NotificationParams(
} title = sharedState.currentProfileName,
withContext(Dispatchers.Main) { stopText = sharedState.stopText,
serviceFlutterEngine = FlutterEngine(GlobalState.application) onlyStatisticsProxy = sharedState.onlyStatisticsProxy
serviceFlutterEngine?.plugins?.add(ServicePlugin()) )
serviceFlutterEngine?.plugins?.add(AppPlugin()) )
serviceFlutterEngine?.plugins?.add(TilePlugin()) Service.setCrashlytics(sharedState.crashlytics)
val dartEntrypoint = DartExecutor.DartEntrypoint( }
FlutterInjector.instance().flutterLoader().findAppBundlePath(), "_service"
) private suspend fun setupAndStart() {
serviceFlutterEngine?.dartExecutor?.executeDartEntrypoint(dartEntrypoint) Service.bind()
} syncState()
} GlobalState.application.showToast(sharedState.startTip)
val initParams = mutableMapOf<String, Any>()
initParams["home-dir"] = GlobalState.application.filesDir.path
initParams["version"] = android.os.Build.VERSION.SDK_INT
val initParamsString = Gson().toJson(initParams)
val setupParamsString = Gson().toJson(sharedState.setupParams)
Service.quickSetup(
initParamsString,
setupParamsString,
onStarted = {
startService()
},
onResult = {
if (it.isNotEmpty()) {
GlobalState.application.showToast(it)
}
},
)
} }
private fun startService() { private fun startService() {
GlobalState.launch { GlobalState.launch {
runLock.withLock { runLock.withLock {
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) { if (runStateFlow.value != RunState.STOP) {
return@launch return@launch
} }
runStateFlow.tryEmit(RunState.PENDING) try {
if (servicePlugin == null) { runStateFlow.tryEmit(RunState.PENDING)
return@launch val options = sharedState.vpnOptions ?: return@launch
} appPlugin?.let {
val options = servicePlugin?.handleGetVpnOptions() it.prepare(options.enable) {
if (options == null) { runTime = Service.startService(options, runTime)
return@launch runStateFlow.tryEmit(RunState.START)
} }
appPlugin?.prepare(options.enable) { } ?: run {
runTime = Service.startService(options, runTime) val intent = VpnService.prepare(GlobalState.application)
runStateFlow.tryEmit(RunState.START) if (intent != null) {
return@launch
}
runTime = Service.startService(options, runTime)
runStateFlow.tryEmit(RunState.START)
}
} finally {
if (runStateFlow.value == RunState.PENDING) {
runStateFlow.tryEmit(RunState.STOP)
}
} }
} }
} }
} }
fun handleStopService() { fun handleStopService() {
GlobalState.launch { GlobalState.launch {
runLock.withLock { runLock.withLock {
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.STOP) { if (runStateFlow.value != RunState.START) {
return@launch return@launch
} }
runStateFlow.tryEmit(RunState.PENDING) try {
runTime = Service.stopService() runStateFlow.tryEmit(RunState.PENDING)
runStateFlow.tryEmit(RunState.STOP) runTime = Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
} finally {
if (runStateFlow.value == RunState.PENDING) {
runStateFlow.tryEmit(RunState.START)
}
}
} }
destroyServiceEngine()
} }
} }
} }

View File

@@ -21,7 +21,9 @@ class TempActivity : Activity(),
} }
QuickAction.STOP.action -> { QuickAction.STOP.action -> {
State.handleStopServiceAction() launch {
State.handleStopServiceAction()
}
} }
QuickAction.TOGGLE.action -> { QuickAction.TOGGLE.action -> {

View File

@@ -45,8 +45,7 @@ class TileService : TileService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent) startActivityAndCollapse(pendingIntent)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION") startActivityAndCollapse(intent)
startActivityAndCollapse(intent)
} }
} }

View File

@@ -1,9 +1,22 @@
package com.follow.clash.models package com.follow.clash.models
import com.follow.clash.service.models.VpnOptions
import com.google.gson.annotations.SerializedName
data class AppState( data class SharedState(
val startTip: String = "Starting VPN...",
val stopTip: String = "Stopping VPN...",
val crashlytics: Boolean = true, val crashlytics: Boolean = true,
val currentProfileName: String = "FlClash", val currentProfileName: String = "FlClash",
val stopText: String = "Stop", val stopText: String = "Stop",
val onlyStatisticsProxy: Boolean = false, val onlyStatisticsProxy: Boolean = false,
val vpnOptions: VpnOptions? = null,
val setupParams: SetupParams? = null,
)
data class SetupParams(
@SerializedName("test-url")
val testUrl: String,
@SerializedName("selected-map")
val selectedMap: Map<String, String>,
) )

View File

@@ -9,7 +9,6 @@ import android.content.pm.ComponentInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
@@ -24,6 +23,7 @@ import com.follow.clash.common.QuickAction
import com.follow.clash.common.quickIntent import com.follow.clash.common.quickIntent
import com.follow.clash.getPackageIconPath import com.follow.clash.getPackageIconPath
import com.follow.clash.models.Package import com.follow.clash.models.Package
import com.follow.clash.showToast
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -193,7 +193,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private fun tip(message: String?) { private fun tip(message: String?) {
Toast.makeText(GlobalState.application, message, Toast.LENGTH_LONG).show() GlobalState.application.showToast(message)
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View File

@@ -3,13 +3,9 @@ package com.follow.clash.plugins
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.Service import com.follow.clash.Service
import com.follow.clash.State import com.follow.clash.State
import com.follow.clash.awaitResult
import com.follow.clash.common.Components import com.follow.clash.common.Components
import com.follow.clash.common.GlobalState
import com.follow.clash.invokeMethodOnMainThread import com.follow.clash.invokeMethodOnMainThread
import com.follow.clash.models.AppState import com.follow.clash.models.SharedState
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@@ -38,7 +34,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"init" -> { "init" -> {
handleInit(call, result) handleInit(result)
} }
"shutdown" -> { "shutdown" -> {
@@ -94,11 +90,6 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
result.success(true) result.success(true)
} }
suspend fun handleGetVpnOptions(): VpnOptions? {
val res = flutterMethodChannel.awaitResult<String>("getVpnOptions", null)
return Gson().fromJson(res, VpnOptions::class.java)
}
val semaphore = Semaphore(10) val semaphore = Semaphore(10)
fun handleSendEvent(value: String?) { fun handleSendEvent(value: String?) {
@@ -116,31 +107,19 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) { private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) {
val data = call.arguments<String>()!! val data = call.arguments<String>()!!
val params = Gson().fromJson(data, AppState::class.java) State.sharedState = Gson().fromJson(data, SharedState::class.java)
GlobalState.setCrashlytics(params.crashlytics)
launch { launch {
Service.updateNotificationParams( State.syncState()
NotificationParams(
title = params.currentProfileName,
stopText = params.stopText,
onlyStatisticsProxy = params.onlyStatisticsProxy
)
)
Service.setCrashlytics(params.crashlytics)
result.success("") result.success("")
} }
} }
fun handleInit(call: MethodCall, result: MethodChannel.Result) {
fun handleInit(result: MethodChannel.Result) {
Service.bind() Service.bind()
launch { launch {
val needSetEventListener = call.arguments<Boolean>() ?: false Service.setEventListener {
when (needSetEventListener) { handleSendEvent(it)
true -> Service.setEventListener {
handleSendEvent(it)
}
false -> Service.setEventListener(null)
}.onSuccess { }.onSuccess {
result.success("") result.success("")
}.onFailure { }.onFailure {

View File

@@ -1,25 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="240" android:viewportWidth="240"
android:viewportHeight="240"> android:viewportHeight="240">
<group android:scaleX="0.924" <group android:scaleX="0.924"
android:scaleY="0.924" android:scaleY="0.924"
android:translateX="9.12" android:translateX="9.12"
android:translateY="9.12"> android:translateY="9.12">
<group android:scaleX="0.63461536" <group android:scaleX="0.63461536"
android:scaleY="0.63461536" android:scaleY="0.63461536"
android:translateX="45.96154" android:translateX="45.96154"
android:translateY="43.846153"> android:translateY="43.846153">
<path <path
android:pathData="M60.65,89.6L154.18,35.6A18,18 107.59,0 1,178.77 42.19L178.77,42.19A18,18 107.59,0 1,172.18 66.78L78.65,120.78A18,18 106.67,0 1,54.06 114.19L54.06,114.19A18,18 106.67,0 1,60.65 89.6z" android:pathData="M60.65,89.6L154.18,35.6A18,18 107.59,0 1,178.77 42.19L178.77,42.19A18,18 107.59,0 1,172.18 66.78L78.65,120.78A18,18 106.67,0 1,54.06 114.19L54.06,114.19A18,18 106.67,0 1,60.65 89.6z"
android:fillColor="#6666FB"/> android:fillColor="#6666FB"/>
<path <path
android:pathData="M84.65,131.17L131.42,104.17A18,18 107.83,0 1,156 110.76L156,110.76A18,18 107.83,0 1,149.42 135.35L102.65,162.35A18,18 106.67,0 1,78.06 155.76L78.06,155.76A18,18 106.67,0 1,84.65 131.17z" android:pathData="M84.65,131.17L131.42,104.17A18,18 107.83,0 1,156 110.76L156,110.76A18,18 107.83,0 1,149.42 135.35L102.65,162.35A18,18 106.67,0 1,78.06 155.76L78.06,155.76A18,18 106.67,0 1,84.65 131.17z"
android:fillColor="#336AB6"/> android:fillColor="#336AB6"/>
<path <path
android:pathData="M108.65,172.74L108.65,172.74A18,18 116.03,0 1,133.24 179.33L133.24,179.33A18,18 116.03,0 1,126.65 203.92L126.65,203.92A18,18 116.03,0 1,102.06 197.33L102.06,197.33A18,18 116.03,0 1,108.65 172.74z" android:pathData="M108.65,172.74L108.65,172.74A18,18 116.03,0 1,133.24 179.33L133.24,179.33A18,18 116.03,0 1,126.65 203.92L126.65,203.92A18,18 116.03,0 1,102.06 197.33L102.06,197.33A18,18 116.03,0 1,108.65 172.74z"
android:fillColor="#5CA8E9"/> android:fillColor="#5CA8E9"/>
</group>
</group> </group>
</group>
</vector> </vector>

View File

@@ -1,19 +1,10 @@
buildscript {
dependencies {
classpath(libs.build.kotlin)
}
}
plugins {
id("com.android.library") apply false
}
allprojects { allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
} }
val newBuildDir: Directory = val newBuildDir: Directory =
rootProject.layout.buildDirectory rootProject.layout.buildDirectory
.dir("../../build") .dir("../../build")
@@ -31,4 +22,3 @@ subprojects {
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory) delete(rootProject.layout.buildDirectory)
} }

View File

@@ -220,7 +220,6 @@ val Long.formatBytes: String
fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List<ByteArray> { fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List<ByteArray> {
val allBytes = toByteArray(charset) val allBytes = toByteArray(charset)
val total = allBytes.size val total = allBytes.size
val maxBytes = when { val maxBytes = when {
total <= 100 * 1024 -> total total <= 100 * 1024 -> total
total <= 1024 * 1024 -> 64 * 1024 total <= 1024 * 1024 -> 64 * 1024

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -59,7 +60,9 @@ class ServiceDelegate<T>(
withTimeout(timeoutMillis) { withTimeout(timeoutMillis) {
val state = serviceState.filterNotNull().first() val state = serviceState.filterNotNull().first()
state.first?.let { state.first?.let {
block(it) withContext(Dispatchers.Default) {
block(it)
}
} ?: throw Exception(state.second) } ?: throw Exception(state.second)
} }
} }

View File

@@ -8,25 +8,9 @@ message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
# set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
add_compile_options(-O3) add_compile_options(-O3 -flto -g0 -fno-exceptions -fno-rtti)
add_link_options(-flto -Wl,--gc-sections,--strip-all)
add_compile_options(-flto)
add_compile_options(-g0)
add_compile_options(-ffunction-sections -fdata-sections)
add_compile_options(-fno-exceptions -fno-rtti)
add_link_options(
-flto
-Wl,--gc-sections
-Wl,--strip-all
-Wl,--exclude-libs=ALL
)
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
endif () endif ()
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so") set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")

View File

@@ -70,6 +70,14 @@ Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean su
suspend(suspended); suspend(suspended);
} }
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_quickSetup(JNIEnv *env, jobject thiz, jstring init_params_string,
jstring setup_params_string, jobject cb) {
const auto interface = new_global(cb);
quickSetup(interface, get_string(init_params_string), get_string(setup_params_string));
}
static jmethodID m_tun_interface_protect; static jmethodID m_tun_interface_protect;
static jmethodID m_tun_interface_resolve_process; static jmethodID m_tun_interface_resolve_process;
@@ -99,12 +107,12 @@ call_tun_interface_resolve_process_impl(void *tun_interface, const int protocol,
const int uid) { const int uid) {
ATTACH_JNI(); ATTACH_JNI();
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod( const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(
static_cast<jobject>(tun_interface), static_cast<jobject>(tun_interface),
m_tun_interface_resolve_process, m_tun_interface_resolve_process,
protocol, protocol,
new_string(source), new_string(source),
new_string(target), new_string(target),
uid)); uid));
return get_string(packageName); return get_string(packageName);
} }
@@ -191,4 +199,10 @@ extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) { Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
} }
#endif
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_quickSetup(JNIEnv *env, jobject thiz, jstring init_params_string,
jstring setup_params_string, jobject cb) {
}
#endif

View File

@@ -102,6 +102,28 @@ data object Core {
} }
} }
fun quickSetup(
initParamsString: String,
setupParamsString: String,
cb: (result: String?) -> Unit,
) {
quickSetup(
initParamsString,
setupParamsString,
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
}
private external fun quickSetup(
initParamsString: String,
setupParamsString: String,
cb: InvokeInterface
)
external fun stopTun() external fun stopTun()
external fun getTraffic(onlyStatisticsProxy: Boolean): String external fun getTraffic(onlyStatisticsProxy: Boolean): String

View File

@@ -0,0 +1,8 @@
// IAckInterface.aidl
package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface IAckInterface {
oneway void onAck();
}

View File

@@ -1,6 +1,8 @@
// ICallbackInterface.aidl // ICallbackInterface.aidl
package com.follow.clash.service; package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface ICallbackInterface { interface ICallbackInterface {
oneway void onResult(in byte[] data,in boolean isSuccess); oneway void onResult(in byte[] data,in boolean isSuccess, in IAckInterface ack);
} }

View File

@@ -1,6 +1,8 @@
// IEventInterface.aidl // IEventInterface.aidl
package com.follow.clash.service; package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface IEventInterface { interface IEventInterface {
oneway void onEvent(in String id, in byte[] data,in boolean isSuccess); oneway void onEvent(in String id, in byte[] data,in boolean isSuccess, in IAckInterface ack);
} }

View File

@@ -4,11 +4,13 @@ package com.follow.clash.service;
import com.follow.clash.service.ICallbackInterface; import com.follow.clash.service.ICallbackInterface;
import com.follow.clash.service.IEventInterface; import com.follow.clash.service.IEventInterface;
import com.follow.clash.service.IResultInterface; import com.follow.clash.service.IResultInterface;
import com.follow.clash.service.IVoidInterface;
import com.follow.clash.service.models.VpnOptions; import com.follow.clash.service.models.VpnOptions;
import com.follow.clash.service.models.NotificationParams; import com.follow.clash.service.models.NotificationParams;
interface IRemoteInterface { interface IRemoteInterface {
void invokeAction(in String data, in ICallbackInterface callback); void invokeAction(in String data, in ICallbackInterface callback);
void quickSetup(in String initParamsString, in String setupParamsString, in ICallbackInterface callback, in IVoidInterface onStarted);
void updateNotificationParams(in NotificationParams params); void updateNotificationParams(in NotificationParams params);
void startService(in VpnOptions options, in long runTime, in IResultInterface result); void startService(in VpnOptions options, in long runTime, in IResultInterface result);
void stopService(in IResultInterface result); void stopService(in IResultInterface result);

View File

@@ -0,0 +1,6 @@
// IVoidInterface.aidl
package com.follow.clash.service;
interface IVoidInterface {
oneway void invoke();
}

View File

@@ -50,7 +50,11 @@ class CommonService : Service(), IBaseService,
} }
override fun start() { override fun start() {
loader.load() try {
loader.load()
} catch (_: Exception) {
stop()
}
} }
override fun stop() { override fun stop() {

View File

@@ -17,8 +17,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.util.UUID import java.util.UUID
import kotlin.coroutines.resume
class RemoteService : Service(), class RemoteService : Service(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) { CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
@@ -75,25 +77,67 @@ class RemoteService : Service(),
private val binder = object : IRemoteInterface.Stub() { private val binder = object : IRemoteInterface.Stub() {
override fun invokeAction(data: String, callback: ICallbackInterface) { override fun invokeAction(data: String, callback: ICallbackInterface) {
Core.invokeAction(data) { Core.invokeAction(data) {
runCatching { launch {
val chunks = it?.chunkedForAidl() ?: listOf() runCatching {
val totalSize = chunks.size val chunks = it?.chunkedForAidl() ?: listOf()
chunks.forEachIndexed { index, chunk -> for ((index, chunk) in chunks.withIndex()) {
callback.onResult(chunk, totalSize - 1 == index) suspendCancellableCoroutine { cont ->
callback.onResult(
chunk,
index == chunks.lastIndex,
object : IAckInterface.Stub() {
override fun onAck() {
cont.resume(Unit)
}
},
)
}
}
} }
} }
} }
} }
override fun quickSetup(
initParamsString: String,
setupParamsString: String,
callback: ICallbackInterface,
onStarted: IVoidInterface
) {
Core.quickSetup(initParamsString, setupParamsString) {
launch {
runCatching {
val chunks = it?.chunkedForAidl() ?: listOf()
for ((index, chunk) in chunks.withIndex()) {
suspendCancellableCoroutine { cont ->
callback.onResult(
chunk,
index == chunks.lastIndex,
object : IAckInterface.Stub() {
override fun onAck() {
cont.resume(Unit)
}
},
)
}
}
}
}
}
onStarted()
}
override fun updateNotificationParams(params: NotificationParams?) { override fun updateNotificationParams(params: NotificationParams?) {
State.notificationParamsFlow.tryEmit(params) State.notificationParamsFlow.tryEmit(params)
} }
override fun startService( override fun startService(
options: VpnOptions, options: VpnOptions,
runtime: Long, runtime: Long,
result: IResultInterface, result: IResultInterface,
) { ) {
GlobalState.log("remote startService")
State.options = options State.options = options
handleStartService(runtime, result) handleStartService(runtime, result)
} }
@@ -106,12 +150,24 @@ class RemoteService : Service(),
GlobalState.log("RemoveEventListener ${eventListener == null}") GlobalState.log("RemoveEventListener ${eventListener == null}")
when (eventListener != null) { when (eventListener != null) {
true -> Core.callSetEventListener { true -> Core.callSetEventListener {
runCatching { launch {
val id = UUID.randomUUID().toString() runCatching {
val chunks = it?.chunkedForAidl() ?: listOf() val id = UUID.randomUUID().toString()
val totalSize = chunks.size val chunks = it?.chunkedForAidl() ?: listOf()
chunks.forEachIndexed { index, chunk -> for ((index, chunk) in chunks.withIndex()) {
eventListener.onEvent(id, chunk, totalSize - 1 == index) suspendCancellableCoroutine { cont ->
eventListener.onEvent(
id,
chunk,
index == chunks.lastIndex,
object : IAckInterface.Stub() {
override fun onAck() {
cont.resume(Unit)
}
},
)
}
}
} }
} }
} }

View File

@@ -187,7 +187,7 @@ class VpnService : SystemVpnService(), IBaseService,
addDnsServer(DNS6) addDnsServer(DNS6)
} }
setMtu(9000) setMtu(9000)
options.accessControl.let { accessControl -> options.accessControlProps.let { accessControl ->
if (accessControl.enable) { if (accessControl.enable) {
when (accessControl.mode) { when (accessControl.mode) {
AccessControlMode.ACCEPT_SELECTED -> { AccessControlMode.ACCEPT_SELECTED -> {
@@ -213,6 +213,7 @@ class VpnService : SystemVpnService(), IBaseService,
allowBypass() allowBypass()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
GlobalState.log("Open http proxy")
setHttpProxy( setHttpProxy(
ProxyInfo.buildDirectProxy( ProxyInfo.buildDirectProxy(
"127.0.0.1", options.port, options.bypassDomain "127.0.0.1", options.port, options.bypassDomain
@@ -233,9 +234,13 @@ class VpnService : SystemVpnService(), IBaseService,
} }
override fun start() { override fun start() {
loader.load() try {
State.options?.let { loader.load()
handleStart(it) State.options?.let {
handleStart(it)
}
} catch (_: Exception) {
stop()
} }
} }

View File

@@ -1,5 +1,6 @@
package com.follow.clash.service.models package com.follow.clash.service.models
import com.follow.clash.common.GlobalState
import com.follow.clash.common.formatBytes import com.follow.clash.common.formatBytes
import com.follow.clash.core.Core import com.follow.clash.core.Core
import com.google.gson.Gson import com.google.gson.Gson
@@ -17,7 +18,8 @@ fun Core.getSpeedTrafficText(onlyStatisticsProxy: Boolean): String {
val res = getTraffic(onlyStatisticsProxy) val res = getTraffic(onlyStatisticsProxy)
val traffic = Gson().fromJson(res, Traffic::class.java) val traffic = Gson().fromJson(res, Traffic::class.java)
return traffic.speedText return traffic.speedText
} catch (_: Exception) { } catch (e: Exception) {
GlobalState.log(e.message + "")
return "" return ""
} }
} }

View File

@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
import java.net.InetAddress import java.net.InetAddress
@Parcelize @Parcelize
data class AccessControl( data class AccessControlProps(
val enable: Boolean, val enable: Boolean,
val mode: AccessControlMode, val mode: AccessControlMode,
val acceptList: List<String>, val acceptList: List<String>,
@@ -19,7 +19,7 @@ data class VpnOptions(
val port: Int, val port: Int,
val ipv6: Boolean, val ipv6: Boolean,
val dnsHijacking: Boolean, val dnsHijacking: Boolean,
val accessControl: AccessControl, val accessControlProps: AccessControlProps,
val allowBypass: Boolean, val allowBypass: Boolean,
val systemProxy: Boolean, val systemProxy: Boolean,
val bypassDomain: List<String>, val bypassDomain: List<String>,

View File

@@ -47,9 +47,6 @@ class NotificationModule(private val service: Service) : Module() {
private val scope = CoroutineScope(Dispatchers.Default) private val scope = CoroutineScope(Dispatchers.Default)
override fun onInstall() { override fun onInstall() {
State.notificationParamsFlow.value?.let {
update(it.extended)
}
scope.launch { scope.launch {
val screenFlow = service.receiveBroadcastFlow { val screenFlow = service.receiveBroadcastFlow {
addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_ON)
@@ -69,6 +66,12 @@ class NotificationModule(private val service: Service) : Module() {
.collect { (params, _) -> .collect { (params, _) ->
update(params!!) update(params!!)
} }
State.notificationParamsFlow.value?.let {
update(it.extended)
} ?: run {
update(NotificationParams().extended)
}
} }
} }
@@ -82,22 +85,24 @@ class NotificationModule(private val service: Service) : Module() {
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent().setComponent(Components.MAIN_ACTIVITY) val intent = Intent().setComponent(Components.MAIN_ACTIVITY)
with(
NotificationCompat.Builder( NotificationCompat.Builder(
service, GlobalState.NOTIFICATION_CHANNEL service, GlobalState.NOTIFICATION_CHANNEL
) ).apply {
) { setSmallIcon(R.drawable.ic_service)
setSmallIcon(R.drawable.ic)
setContentTitle("FlClash") setContentTitle("FlClash")
setContentIntent(intent.toPendingIntent) setContentIntent(intent.toPendingIntent)
setPriority(NotificationCompat.PRIORITY_HIGH) setPriority(NotificationCompat.PRIORITY_HIGH)
setCategory(NotificationCompat.CATEGORY_SERVICE) setCategory(NotificationCompat.CATEGORY_SERVICE)
setOngoing(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
} }
setOngoing(true)
setShowWhen(true) setShowWhen(true)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setRequestPromotedOngoing(true)
}
} }
} }

View File

@@ -1,11 +1,12 @@
pluginManagement { pluginManagement {
val flutterSdkPath = run { val flutterSdkPath =
val properties = java.util.Properties() run {
file("local.properties").inputStream().use { properties.load(it) } val properties = java.util.Properties()
val flutterSdkPath = properties.getProperty("flutter.sdk") file("local.properties").inputStream().use { properties.load(it) }
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } val flutterSdkPath = properties.getProperty("flutter.sdk")
flutterSdkPath require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
} flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

View File

@@ -129,14 +129,8 @@
"compatibleDesc": "Opening it will lose part of its application ability and gain the support of full amount of Clash.", "compatibleDesc": "Opening it will lose part of its application ability and gain the support of full amount of Clash.",
"notSelectedTip": "The current proxy group cannot be selected.", "notSelectedTip": "The current proxy group cannot be selected.",
"tip": "tip", "tip": "tip",
"backupAndRecovery": "Backup and Recovery",
"backupAndRecoveryDesc": "Sync data via WebDAV or file",
"account": "Account", "account": "Account",
"backup": "Backup", "backup": "Backup",
"recovery": "Recovery",
"recoveryProfiles": "Only recovery profiles",
"recoveryAll": "Recovery all data",
"recoverySuccess": "Recovery success",
"backupSuccess": "Backup success", "backupSuccess": "Backup success",
"noInfo": "No info", "noInfo": "No info",
"pleaseBindWebDAV": "Please bind WebDAV", "pleaseBindWebDAV": "Please bind WebDAV",
@@ -219,9 +213,7 @@
"local": "Local", "local": "Local",
"remote": "Remote", "remote": "Remote",
"remoteBackupDesc": "Backup local data to WebDAV", "remoteBackupDesc": "Backup local data to WebDAV",
"remoteRecoveryDesc": "Recovery data from WebDAV",
"localBackupDesc": "Backup local data to local", "localBackupDesc": "Backup local data to local",
"localRecoveryDesc": "Recovery data from file",
"mode": "Mode", "mode": "Mode",
"time": "Time", "time": "Time",
"source": "Source", "source": "Source",
@@ -340,6 +332,8 @@
"none": "none", "none": "none",
"basicConfig": "Basic configuration", "basicConfig": "Basic configuration",
"basicConfigDesc": "Modify the basic configuration globally", "basicConfigDesc": "Modify the basic configuration globally",
"advancedConfig": "Advanced configuration",
"advancedConfigDesc": "Provide diverse configuration options",
"selectedCountTitle": "{count} items have been selected", "selectedCountTitle": "{count} items have been selected",
"addRule": "Add rule", "addRule": "Add rule",
"ruleName": "Rule name", "ruleName": "Rule name",
@@ -379,9 +373,9 @@
"systemApp": "System APP", "systemApp": "System APP",
"noNetworkApp": "No network APP", "noNetworkApp": "No network APP",
"contactMe": "Contact me", "contactMe": "Contact me",
"recoveryStrategy": "Recovery strategy", "restoreStrategy": "Restore strategy",
"recoveryStrategy_override": "Override", "restoreStrategy_override": "Override",
"recoveryStrategy_compatible": "Compatible", "restoreStrategy_compatible": "Compatible",
"logsTest": "Logs test", "logsTest": "Logs test",
"emptyTip": "{label} cannot be empty", "emptyTip": "{label} cannot be empty",
"urlTip": "{label} must be a url", "urlTip": "{label} must be a url",
@@ -390,7 +384,7 @@
"existsTip": "Current {label} already exists", "existsTip": "Current {label} already exists",
"deleteTip": "Are you sure you want to delete the current {label}?", "deleteTip": "Are you sure you want to delete the current {label}?",
"deleteMultipTip": "Are you sure you want to delete the selected {label}?", "deleteMultipTip": "Are you sure you want to delete the selected {label}?",
"nullTip": "No {label} at the moment", "nullTip": "No {label} yet",
"script": "Script", "script": "Script",
"color": "Color", "color": "Color",
"rename": "Rename", "rename": "Rename",
@@ -432,5 +426,55 @@
"dataCollectionTip": "Data Collection Notice", "dataCollectionTip": "Data Collection Notice",
"dataCollectionContent": "This app uses Firebase Crashlytics to collect crash information to improve app stability.\nThe collected data includes device information and crash details, but does not contain personal sensitive data.\nYou can disable this feature in settings.", "dataCollectionContent": "This app uses Firebase Crashlytics to collect crash information to improve app stability.\nThe collected data includes device information and crash details, but does not contain personal sensitive data.\nYou can disable this feature in settings.",
"crashlytics": "Crash Analysis", "crashlytics": "Crash Analysis",
"crashlyticsTip": "When enabled, automatically uploads crash logs without sensitive information when the app crashes" "crashlyticsTip": "When enabled, automatically uploads crash logs without sensitive information when the app crashes",
"appendSystemDns": "Append System DNS",
"appendSystemDnsTip": "Forcefully append system DNS to the configuration",
"editRule": "Edit rule",
"overrideMode": "Override mode",
"standardModeDesc": "Standard mode, override basic configuration, provide simple rule addition capability",
"scriptModeDesc": "Script mode, use external extension scripts, provide one-click override configuration capability",
"addedRules": "Added rules",
"controlGlobalAddedRules": "Control global added rules",
"overrideScript": "Override script",
"goToConfigureScript": "Go to configure script",
"editGlobalRules": "Edit global rules",
"externalFetch": "External fetch",
"confirmForceCrashCore": "Are you sure you want to force crash the core?",
"confirmClearAllData": "Are you sure you want to clear all data?",
"loading": "Loading...",
"loadTest": "Load test",
"yearsAgo": "{count, plural, =1{1 year ago} other{{count} years ago}}",
"monthsAgo": "{count, plural, =1{1 month ago} other{{count} months ago}}",
"daysAgo": "{count, plural, =1{1 day ago} other{{count} days ago}}",
"hoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}",
"minutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}",
"justNow": "Just now",
"noLongerRemind": "Don't remind again",
"accessControlSettings": "Access Control Settings",
"turnOn": "Turn On",
"turnOff": "Turn Off",
"coreConfigChangeDetected": "Core configuration change detected",
"reload": "Reload",
"vpnConfigChangeDetected": "VPN configuration change detected",
"restart": "Restart",
"speedStatistics": "Speed statistics",
"resetPageChangesTip": "The current page has changes. Are you sure you want to reset?",
"overwriteTypeCustom": "Custom",
"overwriteTypeCustomDesc": "Custom mode, fully customize proxy groups and rules",
"unknownNetworkError": "Unknown network error",
"networkRequestException": "Network request exception, please try again later.",
"restoreException": "Recovery exception",
"networkException": "Network exception, please check your connection and try again",
"invalidBackupFile": "Invalid backup file",
"pruneCache": "Prune cache",
"backupAndRestore": "Backup and Restore",
"backupAndRestoreDesc": "Sync data via WebDAV or files",
"restore": "Restore",
"restoreSuccess": "Restore success",
"restoreFromWebDAVDesc": "Restore data via WebDAV",
"restoreFromFileDesc": "Restore data via file",
"restoreOnlyConfig": "Restore configuration files only",
"restoreAllData": "Restore all data",
"addProfile": "Add Profile",
"delayTest": "Delay Test"
} }

View File

@@ -129,14 +129,8 @@
"compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得", "compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
"notSelectedTip": "現在のプロキシグループは選択できません", "notSelectedTip": "現在のプロキシグループは選択できません",
"tip": "ヒント", "tip": "ヒント",
"backupAndRecovery": "バックアップと復元",
"backupAndRecoveryDesc": "WebDAVまたはファイルでデータを同期",
"account": "アカウント", "account": "アカウント",
"backup": "バックアップ", "backup": "バックアップ",
"recovery": "復元",
"recoveryProfiles": "プロファイルのみ復元",
"recoveryAll": "全データ復元",
"recoverySuccess": "復元成功",
"backupSuccess": "バックアップ成功", "backupSuccess": "バックアップ成功",
"noInfo": "情報なし", "noInfo": "情報なし",
"pleaseBindWebDAV": "WebDAVをバインドしてください", "pleaseBindWebDAV": "WebDAVをバインドしてください",
@@ -219,9 +213,7 @@
"local": "ローカル", "local": "ローカル",
"remote": "リモート", "remote": "リモート",
"remoteBackupDesc": "WebDAVにデータをバックアップ", "remoteBackupDesc": "WebDAVにデータをバックアップ",
"remoteRecoveryDesc": "WebDAVからデータを復元",
"localBackupDesc": "ローカルにデータをバックアップ", "localBackupDesc": "ローカルにデータをバックアップ",
"localRecoveryDesc": "ファイルからデータを復元",
"mode": "モード", "mode": "モード",
"time": "時間", "time": "時間",
"source": "ソース", "source": "ソース",
@@ -340,6 +332,8 @@
"none": "なし", "none": "なし",
"basicConfig": "基本設定", "basicConfig": "基本設定",
"basicConfigDesc": "基本設定をグローバルに変更", "basicConfigDesc": "基本設定をグローバルに変更",
"advancedConfig": "高度な設定",
"advancedConfigDesc": "多様な設定を提供",
"selectedCountTitle": "{count} 項目が選択されています", "selectedCountTitle": "{count} 項目が選択されています",
"addRule": "ルールを追加", "addRule": "ルールを追加",
"ruleName": "ルール名", "ruleName": "ルール名",
@@ -380,9 +374,9 @@
"systemApp": "システムアプリ", "systemApp": "システムアプリ",
"noNetworkApp": "ネットワークなしアプリ", "noNetworkApp": "ネットワークなしアプリ",
"contactMe": "連絡する", "contactMe": "連絡する",
"recoveryStrategy": "リカバリー戦略", "restoreStrategy": "復元ストラテジー",
"recoveryStrategy_override": "オーバーライド", "restoreStrategy_override": "上書き",
"recoveryStrategy_compatible": "互換", "restoreStrategy_compatible": "互換",
"logsTest": "ログテスト", "logsTest": "ログテスト",
"emptyTip": "{label}は空欄にできません", "emptyTip": "{label}は空欄にできません",
"urlTip": "{label}はURLである必要があります", "urlTip": "{label}はURLである必要があります",
@@ -391,7 +385,7 @@
"existsTip": "現在の{label}は既に存在しています", "existsTip": "現在の{label}は既に存在しています",
"deleteTip": "現在の{label}を削除してもよろしいですか?", "deleteTip": "現在の{label}を削除してもよろしいですか?",
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?", "deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
"nullTip": "現在{label}はありません", "nullTip": "まだ{label}はありません",
"script": "スクリプト", "script": "スクリプト",
"color": "カラー", "color": "カラー",
"rename": "リネーム", "rename": "リネーム",
@@ -433,5 +427,55 @@
"dataCollectionTip": "データ収集説明", "dataCollectionTip": "データ収集説明",
"dataCollectionContent": "本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。", "dataCollectionContent": "本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。",
"crashlytics": "クラッシュ分析", "crashlytics": "クラッシュ分析",
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします" "crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
"appendSystemDns": "システムDNSを追加",
"appendSystemDnsTip": "設定にシステムDNSを強制的に追加します",
"editRule": "ルールを編集",
"overrideMode": "上書きモード",
"standardModeDesc": "標準モード、基本設定を上書きし、シンプルなルール追加機能を提供",
"scriptModeDesc": "スクリプトモード、外部拡張スクリプトを使用し、ワンクリックで設定を上書きする機能を提供",
"addedRules": "追加ルール",
"controlGlobalAddedRules": "グローバル追加ルールを制御",
"overrideScript": "上書きスクリプト",
"goToConfigureScript": "スクリプト設定に移動",
"editGlobalRules": "グローバルルールを編集",
"externalFetch": "外部取得",
"confirmForceCrashCore": "コアを強制的にクラッシュさせてもよろしいですか?",
"confirmClearAllData": "すべてのデータをクリアしてもよろしいですか?",
"loading": "読み込み中...",
"loadTest": "読み込みテスト",
"yearsAgo": "{count}年前",
"monthsAgo": "{count}ヶ月前",
"daysAgo": "{count}日前",
"hoursAgo": "{count}時間前",
"minutesAgo": "{count}分前",
"justNow": "たった今",
"noLongerRemind": "今後表示しない",
"accessControlSettings": "アクセス制御設定",
"turnOn": "オン",
"turnOff": "オフ",
"coreConfigChangeDetected": "コア設定の変更が検出されました",
"reload": "リロード",
"vpnConfigChangeDetected": "VPN設定の変更が検出されました",
"restart": "再起動",
"speedStatistics": "速度統計",
"resetPageChangesTip": "現在のページに変更があります。リセットしてもよろしいですか?",
"overwriteTypeCustom": "カスタム",
"overwriteTypeCustomDesc": "カスタムモード、プロキシグループとルールを完全にカスタマイズ可能",
"unknownNetworkError": "不明なネットワークエラー",
"networkRequestException": "ネットワーク要求例外、後でもう一度試してください。",
"restoreException": "復元例外",
"networkException": "ネットワーク例外、接続を確認してもう一度お試しください",
"invalidBackupFile": "無効なバックアップファイル",
"pruneCache": "キャッシュの削除",
"backupAndRestore": "バックアップと復元",
"backupAndRestoreDesc": "WebDAVまたはファイルを介してデータを同期する",
"restore": "復元",
"restoreSuccess": "復元に成功しました",
"restoreFromWebDAVDesc": "WebDAVを介してデータを復元する",
"restoreFromFileDesc": "ファイルを介してデータを復元する",
"restoreOnlyConfig": "設定ファイルのみを復元する",
"restoreAllData": "すべてのデータを復元する",
"addProfile": "プロファイルを追加",
"delayTest": "遅延テスト"
} }

View File

@@ -340,6 +340,8 @@
"none": "Нет", "none": "Нет",
"basicConfig": "Базовая конфигурация", "basicConfig": "Базовая конфигурация",
"basicConfigDesc": "Глобальное изменение базовых настроек", "basicConfigDesc": "Глобальное изменение базовых настроек",
"advancedConfig": "Расширенная конфигурация",
"advancedConfigDesc": "Предоставляет разнообразные варианты конфигурации",
"selectedCountTitle": "Выбрано {count} элементов", "selectedCountTitle": "Выбрано {count} элементов",
"addRule": "Добавить правило", "addRule": "Добавить правило",
"ruleName": "Название правила", "ruleName": "Название правила",
@@ -380,9 +382,9 @@
"systemApp": "Системное приложение", "systemApp": "Системное приложение",
"noNetworkApp": "Приложение без сети", "noNetworkApp": "Приложение без сети",
"contactMe": "Свяжитесь со мной", "contactMe": "Свяжитесь со мной",
"recoveryStrategy": "Стратегия восстановления", "restoreStrategy": "Стратегия восстановления",
"recoveryStrategy_override": "Переопределение", "restoreStrategy_override": "Перезаписать",
"recoveryStrategy_compatible": "Совместимый", "restoreStrategy_compatible": "Совместимый",
"logsTest": "Тест журналов", "logsTest": "Тест журналов",
"emptyTip": "{label} не может быть пустым", "emptyTip": "{label} не может быть пустым",
"urlTip": "{label} должен быть URL", "urlTip": "{label} должен быть URL",
@@ -391,7 +393,7 @@
"existsTip": "Текущий {label} уже существует", "existsTip": "Текущий {label} уже существует",
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?", "deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?", "deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
"nullTip": "Сейчас {label} нет", "nullTip": "{label} пока отсутствуют",
"script": "Скрипт", "script": "Скрипт",
"color": "Цвет", "color": "Цвет",
"rename": "Переименовать", "rename": "Переименовать",
@@ -433,5 +435,55 @@
"dataCollectionTip": "Уведомление о сборе данных", "dataCollectionTip": "Уведомление о сборе данных",
"dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.", "dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.",
"crashlytics": "Анализ сбоев", "crashlytics": "Анализ сбоев",
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя" "crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
"appendSystemDns": "Добавить системный DNS",
"appendSystemDnsTip": "Принудительно добавить системный DNS к конфигурации",
"editRule": "Редактировать правило",
"overrideMode": "Режим переопределения",
"standardModeDesc": "Стандартный режим, переопределение базовой конфигурации, предоставление возможности простого добавления правил",
"scriptModeDesc": "Режим скрипта, использование внешних расширяющих скриптов, предоставление возможности переопределения конфигурации одним кликом",
"addedRules": "Добавленные правила",
"controlGlobalAddedRules": "Управление глобальными добавленными правилами",
"overrideScript": "Скрипт переопределения",
"goToConfigureScript": "Перейти к настройке скрипта",
"editGlobalRules": "Редактировать глобальные правила",
"externalFetch": "Внешнее получение",
"confirmForceCrashCore": "Вы уверены, что хотите принудительно аварийно завершить работу ядра?",
"confirmClearAllData": "Вы уверены, что хотите очистить все данные?",
"loading": "Загрузка...",
"loadTest": "Тест загрузки",
"yearsAgo": "{count, plural, one{{count} год назад} few{{count} года назад} many{{count} лет назад} other{{count} года назад}}",
"monthsAgo": "{count, plural, one{{count} месяц назад} few{{count} месяца назад} many{{count} месяцев назад} other{{count} месяца назад}}",
"daysAgo": "{count, plural, one{{count} день назад} few{{count} дня назад} many{{count} дней назад} other{{count} дня назад}}",
"hoursAgo": "{count, plural, one{{count} час назад} few{{count} часа назад} many{{count} часов назад} other{{count} часа назад}}",
"minutesAgo": "{count, plural, one{{count} минута назад} few{{count} минуты назад} many{{count} минут назад} other{{count} минуты назад}}",
"justNow": "Только что",
"noLongerRemind": "Больше не напоминать",
"accessControlSettings": "Настройки контроля доступа",
"turnOn": "Включить",
"turnOff": "Выключить",
"coreConfigChangeDetected": "Обнаружено изменение конфигурации ядра",
"reload": "Перезагрузить",
"vpnConfigChangeDetected": "Обнаружено изменение конфигурации VPN",
"restart": "Перезапустить",
"speedStatistics": "Статистика скорости",
"resetPageChangesTip": "На текущей странице есть изменения. Вы уверены, что хотите сбросить?",
"overwriteTypeCustom": "Пользовательский",
"overwriteTypeCustomDesc": "Пользовательский режим, полная настройка групп прокси и правил",
"unknownNetworkError": "Неизвестная сетевая ошибка",
"networkRequestException": "Исключение сетевого запроса, пожалуйста, попробуйте позже.",
"restoreException": "Ошибка восстановления",
"networkException": "Ошибка сети, проверьте соединение и попробуйте еще раз",
"invalidBackupFile": "Неверный файл резервной копии",
"pruneCache": "Очистить кэш",
"backupAndRestore": "Резервное копирование и восстановление",
"backupAndRestoreDesc": "Синхронизация данных через WebDAV или файлы",
"restore": "Восстановить",
"restoreSuccess": "Восстановление успешно",
"restoreFromWebDAVDesc": "Восстановить данные через WebDAV",
"restoreFromFileDesc": "Восстановить данные из файла",
"restoreOnlyConfig": "Восстановить только файлы конфигурации",
"restoreAllData": "Восстановить все данные",
"addProfile": "Добавить профиль",
"delayTest": "Тест задержки"
} }

View File

@@ -129,14 +129,8 @@
"compatibleDesc": "开启将失去部分应用能力获得全量的Clash的支持", "compatibleDesc": "开启将失去部分应用能力获得全量的Clash的支持",
"notSelectedTip": "当前代理组无法选中", "notSelectedTip": "当前代理组无法选中",
"tip": "提示", "tip": "提示",
"backupAndRecovery": "备份与恢复",
"backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
"account": "账号", "account": "账号",
"backup": "备份", "backup": "备份",
"recovery": "恢复",
"recoveryProfiles": "仅恢复配置文件",
"recoveryAll": "恢复所有数据",
"recoverySuccess": "恢复成功",
"backupSuccess": "备份成功", "backupSuccess": "备份成功",
"noInfo": "暂无信息", "noInfo": "暂无信息",
"pleaseBindWebDAV": "请绑定WebDAV", "pleaseBindWebDAV": "请绑定WebDAV",
@@ -219,9 +213,7 @@
"local": "本地", "local": "本地",
"remote": "远程", "remote": "远程",
"remoteBackupDesc": "备份数据到WebDAV", "remoteBackupDesc": "备份数据到WebDAV",
"remoteRecoveryDesc": "通过WebDAV恢复数据",
"localBackupDesc": "备份数据到本地", "localBackupDesc": "备份数据到本地",
"localRecoveryDesc": "通过文件恢复数据",
"mode": "模式", "mode": "模式",
"time": "时间", "time": "时间",
"source": "来源", "source": "来源",
@@ -340,6 +332,8 @@
"none": "无", "none": "无",
"basicConfig": "基本配置", "basicConfig": "基本配置",
"basicConfigDesc": "全局修改基本配置", "basicConfigDesc": "全局修改基本配置",
"advancedConfig": "进阶配置",
"advancedConfigDesc": "提供多样化配置",
"selectedCountTitle": "已选择 {count} 项", "selectedCountTitle": "已选择 {count} 项",
"addRule": "添加规则", "addRule": "添加规则",
"ruleName": "规则名称", "ruleName": "规则名称",
@@ -380,9 +374,9 @@
"systemApp": "系统应用", "systemApp": "系统应用",
"noNetworkApp": "无网络应用", "noNetworkApp": "无网络应用",
"contactMe": "联系我", "contactMe": "联系我",
"recoveryStrategy": "恢复策略", "restoreStrategy": "恢复策略",
"recoveryStrategy_override": "覆盖", "restoreStrategy_override": "覆盖",
"recoveryStrategy_compatible": "兼容", "restoreStrategy_compatible": "兼容",
"logsTest": "日志测试", "logsTest": "日志测试",
"emptyTip": "{label}不能为空", "emptyTip": "{label}不能为空",
"urlTip": "{label}必须为URL", "urlTip": "{label}必须为URL",
@@ -433,5 +427,55 @@
"dataCollectionTip": "数据收集说明", "dataCollectionTip": "数据收集说明",
"dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情不包含个人敏感数据。\n您可以在设置中关闭此功能。", "dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情不包含个人敏感数据。\n您可以在设置中关闭此功能。",
"crashlytics": "崩溃分析", "crashlytics": "崩溃分析",
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志" "crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
"appendSystemDns": "追加系统DNS",
"appendSystemDnsTip": "强制为配置附加系统DNS",
"editRule": "编辑规则",
"overrideMode": "覆写模式",
"standardModeDesc": "标准模式,覆写基本配置,提供简单追加规则能力",
"scriptModeDesc": "脚本模式,使用外部扩展脚本,提供一键覆写配置的能力",
"addedRules": "附加规则",
"controlGlobalAddedRules": "控制全局附加规则",
"overrideScript": "覆写脚本",
"goToConfigureScript": "前往配置脚本",
"editGlobalRules": "编辑全局规则",
"externalFetch": "外部获取",
"confirmForceCrashCore": "确定要强制崩溃核心?",
"confirmClearAllData": "确定要清除所有数据?",
"loading": "加载中...",
"loadTest": "加载测试",
"yearsAgo": "{count} 年前",
"monthsAgo": "{count} 个月前",
"daysAgo": "{count} 天前",
"hoursAgo": "{count} 小时前",
"minutesAgo": "{count} 分钟前",
"justNow": "刚刚",
"noLongerRemind": "不再提示",
"accessControlSettings": "访问控制设置",
"turnOn": "开启",
"turnOff": "关闭",
"coreConfigChangeDetected": "检测到核心配置更改",
"reload": "重载",
"vpnConfigChangeDetected": "检测到VPN相关配置改动",
"restart": "重启",
"speedStatistics": "网速统计",
"resetPageChangesTip": "当前页面存在更改,确定重置吗?",
"overwriteTypeCustom": "自定义",
"overwriteTypeCustomDesc": "自定义模式,支持完全自定义修改代理组以及规则",
"unknownNetworkError": "未知网络错误",
"networkRequestException": "网络请求异常,请稍后再试。",
"restoreException": "恢复异常",
"networkException": "网络异常,请检查连接后重试",
"invalidBackupFile": "无效备份文件",
"pruneCache": "修剪缓存",
"backupAndRestore": "备份与恢复",
"backupAndRestoreDesc": "通过WebDAV或者文件同步数据",
"restore": "恢复",
"restoreSuccess": "恢复成功",
"restoreFromWebDAVDesc": "通过WebDAV恢复数据",
"restoreFromFileDesc": "通过文件恢复数据",
"restoreOnlyConfig": "仅恢复配置文件",
"restoreAllData": "恢复所有数据",
"addProfile": "添加配置",
"delayTest": "延迟测试"
} }

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,23 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="75" y="50" width="150" height="180" rx="24" fill="#FDF7FF" stroke="#E8DEF8" stroke-width="2"/>
<rect x="95" y="90" width="36" height="12" rx="4" fill="#E8DEF8"/>
<rect x="140" y="90" width="65" height="12" rx="6" fill="#E8DEF8"/>
<path d="M95 118H205" stroke="#E8DEF8" stroke-width="2" stroke-dasharray="4 4"/>
<rect x="95" y="138" width="40" height="12" rx="6" fill="#E8DEF8" opacity="0.7"/>
<rect x="145" y="138" width="50" height="12" rx="6" fill="#E8DEF8" opacity="0.5"/>
<rect x="95" y="162" width="55" height="12" rx="6" fill="#E8DEF8" opacity="0.7"/>
<rect x="160" y="162" width="30" height="12" rx="6" fill="#E8DEF8" opacity="0.5"/>
<g transform="translate(210, 210)">
<circle cx="0" cy="0" r="38" fill="#6750A4" stroke="#FDF7FF" stroke-width="6"/>
<path d="M-10 16V-16M-10 -16L-18 -8M-10 -16L-2 -8" stroke="#FDF7FF" stroke-width="5" stroke-linecap="round"
stroke-linejoin="round"/>
<path d="M10 -16V16M10 16L2 8M10 16L18 8" stroke="#FDF7FF" stroke-width="5" stroke-linecap="round"
stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,9 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="75" y="50" width="150" height="180" rx="24" fill="#FDF7FF" stroke="#E8DEF8" stroke-width="2"/>
<rect x="100" y="90" width="100" height="12" rx="6" fill="#E8DEF8"/>
<rect x="100" y="115" width="70" height="12" rx="6" fill="#E8DEF8"/>
<rect x="100" y="140" width="80" height="12" rx="6" fill="#E8DEF8"/>
<rect x="155" y="170" width="80" height="60" rx="12" fill="#6750A4"/>
<rect x="150" y="165" width="90" height="18" rx="6" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"/>
<rect x="185" y="200" width="20" height="6" rx="3" fill="#FDF7FF" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 700 B

View File

@@ -0,0 +1,25 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M90 200C90 180 100 165 150 165C200 165 210 180 210 200V220C210 231.046 201.046 240 190 240H110C98.9543 240 90 231.046 90 220V200Z"
fill="#E8DEF8"/>
<rect x="75" y="85" width="150" height="100" rx="30" fill="#6750A4"/>
<rect x="85" y="95" width="130" height="80" rx="22" fill="#6750A4" stroke="#7D66B5" stroke-width="2"/>
<path d="M110 135 C110 142 118 148 128 148 C138 148 146 142 146 135" stroke="#FDF7FF" stroke-width="4"
stroke-linecap="round"/>
<path d="M154 135 C154 142 162 148 172 148 C182 148 190 142 190 135" stroke="#FDF7FF" stroke-width="4"
stroke-linecap="round"/>
<circle cx="150" cy="160" r="4" fill="#E8DEF8" opacity="0.5"/>
<path d="M150 85 V 65" stroke="#6750A4" stroke-width="4" stroke-linecap="round"/>
<circle cx="150" cy="60" r="8" fill="#6750A4"/>
<circle cx="150" cy="60" r="3" fill="#FDF7FF"/>
<path d="M220 70 L235 70 L220 85 H235" stroke="#6750A4" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round"/>
<path d="M245 40 L255 40 L245 50 H255" stroke="#E8DEF8" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
<path d="M90 185 H210" stroke="#000" stroke-width="4" stroke-opacity="0.1" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,37 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60 94V84C60 72.9543 68.9543 64 80 64H130C141.046 64 150 72.9543 150 84V94H220C231.046 94 240 102.954 240 114V210C240 221.046 231.046 230 220 230H80C68.9543 230 60 221.046 60 210V94Z"
fill="#FDF7FF" stroke="#E8DEF8" stroke-width="2"/>
<rect x="90" y="124" width="60" height="12" rx="6" fill="#E8DEF8"/>
<rect x="90" y="154" width="50" height="12" rx="6" fill="#E8DEF8"/>
<rect x="90" y="184" width="40" height="12" rx="6" fill="#E8DEF8"/>
<rect x="180" y="184" width="30" height="12" rx="6" fill="#E8DEF8" opacity="0.6"/>
<circle cx="186" cy="190" r="6" fill="#E8DEF8"/>
<g transform="translate(210, 210)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 -32 C-17.67 -32 -32 -17.67 -32 0 C-32 17.67 -17.67 32 0 32 C17.67 32 32 17.67 32 0 C32 -17.67 17.67 -32 0 -32ZM0 -8 C-4.42 -8 -8 -4.42 -8 0 C-8 4.42 -4.42 8 0 8 C4.42 8 8 4.42 8 0 C8 -4.42 4.42 -8 0 -8Z"
fill="#6750A4" stroke="#FDF7FF" stroke-width="6"/>
<rect x="-5" y="-38" width="10" height="12" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"/>
<rect x="-5" y="26" width="10" height="12" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"/>
<rect x="-38" y="-5" width="12" height="10" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"/>
<rect x="26" y="-5" width="12" height="10" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"/>
<rect x="-5" y="-38" width="10" height="12" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"
transform="rotate(45)"/>
<rect x="-5" y="26" width="10" height="12" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"
transform="rotate(45)"/>
<rect x="-38" y="-5" width="12" height="10" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"
transform="rotate(45)"/>
<rect x="26" y="-5" width="12" height="10" rx="3" fill="#6750A4" stroke="#FDF7FF" stroke-width="2"
transform="rotate(45)"/>
<circle cx="0" cy="0" r="22" fill="#6750A4"/>
<circle cx="0" cy="0" r="8" fill="#FDF7FF" opacity="0.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,18 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="75" y="50" width="150" height="180" rx="24" fill="#FDF7FF" stroke="#E8DEF8" stroke-width="2"/>
<rect x="95" y="90" width="110" height="16" rx="4" fill="#E8DEF8"/>
<circle cx="105" cy="98" r="3" fill="#FDF7FF"/>
<circle cx="115" cy="98" r="3" fill="#FDF7FF"/>
<rect x="95" y="120" width="110" height="16" rx="4" fill="#E8DEF8"/>
<circle cx="105" cy="128" r="3" fill="#FDF7FF"/>
<circle cx="115" cy="128" r="3" fill="#FDF7FF"/>
<rect x="95" y="150" width="80" height="16" rx="4" fill="#E8DEF8"/>
<circle cx="105" cy="158" r="3" fill="#FDF7FF"/>
<circle cx="115" cy="158" r="3" fill="#FDF7FF"/>
<circle cx="195" cy="195" r="30" fill="#6750A4"/>
<rect x="180" y="193" width="30" height="4" rx="2" fill="#FDF7FF" transform="rotate(45 195 195)"/>
<circle cx="183" cy="183" r="4" fill="#FDF7FF"/>
<circle cx="207" cy="207" r="4" fill="#FDF7FF"/>
<path d="M175 158 H190 V165" stroke="#E8DEF8" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,31 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="75" y="50" width="150" height="180" rx="24" fill="#FDF7FF" stroke="#E8DEF8" stroke-width="2"/>
<rect x="95" y="90" width="80" height="12" rx="6" fill="#E8DEF8"/>
<rect x="185" y="90" width="25" height="12" rx="6" fill="#E8DEF8" opacity="0.7"/>
<rect x="95" y="125" width="60" height="12" rx="6" fill="#E8DEF8"/>
<rect x="165" y="125" width="45" height="12" rx="6" fill="#E8DEF8" opacity="0.7"/>
<rect x="95" y="160" width="70" height="12" rx="6" fill="#E8DEF8"/>
<rect x="175" y="160" width="35" height="12" rx="6" fill="#E8DEF8" opacity="0.7"/>
<g transform="translate(210, 210)">
<circle cx="0" cy="0" r="36" fill="#6750A4" stroke="#FDF7FF" stroke-width="6"/>
<circle cx="0" cy="0" r="24" stroke="#FDF7FF" stroke-width="3"/>
<path d="M-24 0C-24 0 -12 8 0 8C12 8 24 0 24 0" stroke="#FDF7FF" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round"/>
<ellipse cx="0" cy="0" rx="10" ry="24" stroke="#FDF7FF" stroke-width="3"/>
<circle cx="14" cy="-12" r="3" fill="#FDF7FF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,19 @@
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="75" y="50" width="150" height="180" rx="24" fill="#FDF7FF" stroke="#E8DEF8" stroke-width="2"/>
<rect x="100" y="90" width="30" height="12" rx="6" fill="#E8DEF8"/>
<rect x="136" y="90" width="50" height="12" rx="6" fill="#E8DEF8"/>
<rect x="120" y="120" width="80" height="12" rx="6" fill="#E8DEF8"/>
<rect x="120" y="150" width="50" height="12" rx="6" fill="#E8DEF8"/>
<rect x="100" y="180" width="20" height="12" rx="6" fill="#E8DEF8"/>
<g transform="translate(165, 160)">
<rect x="0" y="0" width="80" height="80" rx="20" fill="#6750A4" stroke="#FDF7FF" stroke-width="4"/>
<path d="M28 30L18 40L28 50" stroke="#FDF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M52 30L62 40L52 50" stroke="#FDF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M46 26L34 54" stroke="#FDF7FF" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -6,6 +6,7 @@ targets:
build_extensions: build_extensions:
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart' '^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
'^lib/database/{{}}.dart': 'lib/database/generated/{{}}.g.dart'
freezed: freezed:
options: options:
build_extensions: build_extensions:

View File

@@ -53,8 +53,8 @@ func handleAction(action *Action, result ActionResult) {
result.success(handleShutdown()) result.success(handleShutdown())
return return
case validateConfigMethod: case validateConfigMethod:
data := []byte(action.Data.(string)) path := action.Data.(string)
result.success(handleValidateConfig(data)) result.success(handleValidateConfig(path))
return return
case updateConfigMethod: case updateConfigMethod:
data := []byte(action.Data.(string)) data := []byte(action.Data.(string))

View File

@@ -17,6 +17,7 @@ import (
"github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route" "github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
@@ -24,6 +25,7 @@ import (
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"sync" "sync"
) )
@@ -35,12 +37,6 @@ var (
mBatch, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) mBatch, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
) )
type ExternalProviders []ExternalProvider
func (a ExternalProviders) Len() int { return len(a) }
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func getExternalProvidersRaw() map[string]cp.Provider { func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider) eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() { for n, p := range tunnel.Providers() {
@@ -239,30 +235,13 @@ func updateConfig(params *UpdateParams) {
updateListeners() updateListeners()
} }
func parseWithPath(path string) (*config.Config, error) { func applyConfig(params *SetupParams) error {
buf, err := readFile(path) runtime.GC()
if err != nil {
return nil, err
}
rawConfig := config.DefaultRawConfig()
err = UnmarshalJson(buf, rawConfig)
if err != nil {
return nil, err
}
parseRawConfig, err := config.ParseRawConfig(rawConfig)
if err != nil {
return nil, err
}
return parseRawConfig, nil
}
func setupConfig(params *SetupParams) error {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
var err error var err error
constant.DefaultTestURL = params.TestURL constant.DefaultTestURL = params.TestURL
currentConfig, err = parseWithPath(filepath.Join(constant.Path.HomeDir(), "config.json")) currentConfig, err = executor.ParseWithPath(filepath.Join(constant.Path.HomeDir(), "config.yaml"))
if err != nil { if err != nil {
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig()) currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
} }

View File

@@ -66,6 +66,11 @@ type ExternalProvider struct {
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"` SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
} }
type ProxiesData struct {
Proxies map[string]constant.Proxy `json:"proxies"`
All []string `json:"all"`
}
const ( const (
messageMethod Method = "message" messageMethod Method = "message"
initClashMethod Method = "initClash" initClashMethod Method = "initClash"

View File

@@ -1,6 +1,6 @@
module core module core
go 1.25 go 1.20
replace github.com/metacubex/mihomo => ./Clash.Meta replace github.com/metacubex/mihomo => ./Clash.Meta
@@ -10,6 +10,7 @@ require (
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/RyuaNerin/go-krypto v1.3.0 // indirect github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
@@ -18,87 +19,85 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/enfein/mieru/v3 v3.26.2 // indirect
github.com/enfein/mieru/v3 v3.19.1 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect github.com/gofrs/uuid/v5 v5.4.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a // indirect github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d // indirect
github.com/metacubex/ascon v0.1.0 // indirect github.com/metacubex/ascon v0.1.0 // indirect
github.com/metacubex/bart v0.20.5 // indirect github.com/metacubex/bart v0.26.0 // indirect
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b // indirect github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b // indirect
github.com/metacubex/blake3 v0.1.0 // indirect github.com/metacubex/blake3 v0.1.0 // indirect
github.com/metacubex/chacha v0.1.5 // indirect github.com/metacubex/chacha v0.1.5 // indirect
github.com/metacubex/chi v0.1.0 // indirect
github.com/metacubex/cpu v0.1.0 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
github.com/metacubex/hkdf v0.1.0 // indirect
github.com/metacubex/hpke v0.1.0 // indirect
github.com/metacubex/http v0.1.0 // indirect
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect
github.com/metacubex/mlkem v0.1.0 // indirect
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 // indirect github.com/metacubex/qpack v0.6.0 // indirect
github.com/metacubex/quic-go v0.59.1-0.20260112033758-aa29579f2001 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/restls-client-go v0.1.7 // indirect github.com/metacubex/restls-client-go v0.1.7 // indirect
github.com/metacubex/sing v0.5.5 // indirect github.com/metacubex/sing v0.5.6 // indirect
github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac // indirect github.com/metacubex/sing-mux v0.3.4 // indirect
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb // indirect github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e // indirect
github.com/metacubex/sing-shadowsocks v0.2.12 // indirect github.com/metacubex/sing-shadowsocks v0.2.12 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.6 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.7 // indirect github.com/metacubex/sing-tun v0.4.11 // indirect
github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0 // indirect github.com/metacubex/sing-vmess v0.2.4 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // 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/smux v0.0.0-20260105030934-d0c8756d3141 // indirect
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 // indirect github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // indirect
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 // indirect github.com/metacubex/tls v0.1.1 // indirect
github.com/metacubex/utls v1.8.4 // indirect
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f // indirect
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect
github.com/miekg/dns v1.1.63 // indirect github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/openacid/low v0.1.21 // indirect github.com/openacid/low v0.1.21 // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/samber/lo v1.51.0 // indirect github.com/samber/lo v1.52.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/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.33.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
@@ -106,7 +105,7 @@ require (
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg= 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/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
@@ -12,9 +14,6 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -22,10 +21,8 @@ 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/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 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/enfein/mieru/v3 v3.26.2 h1:U/2XJc+3vrJD9r815FoFdwToQFEcqSOzzzWIPPhjfEU=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/enfein/mieru/v3 v3.26.2/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/enfein/mieru/v3 v3.19.1 h1:19b9kgFC7oJXX9RLEO5Pi1gO6yek5cWlpK7IJVUoE8I=
github.com/enfein/mieru/v3 v3.19.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= 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/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 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -39,37 +36,26 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 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/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 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -78,76 +64,94 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc=
github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a h1:c1QSGpacSeQdBdWcEKZKGuWLcqIG2wxHEygAcXuDwS4= github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c=
github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY= github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY=
github.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM= github.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM=
github.com/metacubex/ascon v0.1.0/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc= github.com/metacubex/ascon v0.1.0/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc=
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM= github.com/metacubex/bart v0.26.0 h1:d/bBTvVatfVWGfQbiDpYKI1bXUJgjaabB2KpK1Tnk6w=
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bart v0.26.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY=
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw=
github.com/metacubex/blake3 v0.1.0 h1:KGnjh/56REO7U+cgZA8dnBhxdP7jByrG7hTP+bu6cqY= github.com/metacubex/blake3 v0.1.0 h1:KGnjh/56REO7U+cgZA8dnBhxdP7jByrG7hTP+bu6cqY=
github.com/metacubex/blake3 v0.1.0/go.mod h1:CCkLdzFrqf7xmxCdhQFvJsRRV2mwOLDoSPg6vUTB9Uk= github.com/metacubex/blake3 v0.1.0/go.mod h1:CCkLdzFrqf7xmxCdhQFvJsRRV2mwOLDoSPg6vUTB9Uk=
github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M= 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/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/chi v0.1.0 h1:rjNDyDj50nRpicG43CNkIw4ssiCbmDL8d7wJXKlUCsg=
github.com/metacubex/chi v0.1.0/go.mod h1:zM5u5oMQt8b2DjvDHvzadKrP6B2ztmasL1YHRMbVV+g=
github.com/metacubex/cpu v0.1.0 h1:8PeTdV9j6UKbN1K5Jvtbi/Jock7dknvzyYuLb8Conmk=
github.com/metacubex/cpu v0.1.0/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU= 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/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 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
github.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I=
github.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo= 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/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 h1:8JVlYuE8uSJAvmyCd4TjvDxs57xjb0WxEoaWafK5+qs= github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c= github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
github.com/metacubex/quic-go v0.59.1-0.20260112033758-aa29579f2001 h1:RlT3bFCIDM/NR9GWaDbFCrweOwpHRfgaT9c0zuRlPhY=
github.com/metacubex/quic-go v0.59.1-0.20260112033758-aa29579f2001/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.6 h1:mEPDCadsCj3DB8gn+t/EtposlYuALEkExa/LUguw6/c=
github.com/metacubex/sing v0.5.5 h1:m5U8iHvRAUxlme3FZlE/LPIGHjU8oMCUzXWGbQQAC1E= github.com/metacubex/sing v0.5.6/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.5/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.4 h1:tf4r27CIkzaxq9kBlAXQkgMXq2HPp5Mta60Kb4RCZF0=
github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac h1:wDH/Jh/yqWbzPktqJP+Y1cUG8hchcrzKzUxJiSpnaQs= github.com/metacubex/sing-mux v0.3.4/go.mod h1:SEJfAuykNj/ozbPqngEYqyggwSr81+L7Nu09NRD5mh4=
github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e h1:MLxp42z9Jd6LtY2suyawnl24oNzIsFxWc15bNeDIGxA=
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb h1:U/m3h8lp/j7i8zFgfvScLdZa1/Y8dd74oO7iZaQq80s= github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e/go.mod h1:+lgKTd52xAarGtqugALISShyw4KxnoEpYe2u0zJh26w=
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
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 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-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.7 h1:ZDY/W+1c7PeWWKeKRyUo18fySF/TWjB0i5ui81Ar778= github.com/metacubex/sing-tun v0.4.11 h1:NG5zpvYPbBXf+9GSUmDaGCDwl3hZXV677tbRAw0QtCM=
github.com/metacubex/sing-tun v0.4.7/go.mod h1:xHecZRwBnKWe6zG9amAK9cXf91lF6blgjBqm+VvOrmU= github.com/metacubex/sing-tun v0.4.11/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0 h1:WZepq4TOZa6WewB8tGAZrrL+bL2R2ivoBzuEgAeolWc= github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkAaus6I=
github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM= github.com/metacubex/sing-vmess v0.2.4/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU= 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/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-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 h1:yN3mQ4cT9sPUciw/rO0Isc/8QlO86DB6g9SEMRgQ8Cw= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 h1:csEbKOzRAxJXffOeZnnS3/kA/F55JiTbKv5jcYqCXms= github.com/metacubex/tls v0.1.1 h1:BEcZrsPTTfNf4sKZ02EbZodv4UIj7fgHWa1Eqo12Bc0=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U= github.com/metacubex/tls v0.1.1/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E=
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49/go.mod h1:MBeEa9IVBphH7vc3LNtW6ZujVXFizotPo3OEiHQ+TNU=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
@@ -160,18 +164,10 @@ github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
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/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= 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/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
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= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -181,21 +177,10 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@@ -209,14 +194,12 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -240,30 +223,24 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 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/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
@@ -12,16 +13,18 @@ import (
"github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/exp/slices"
"net" "net"
"os" "os"
"runtime" "runtime"
"sort" "runtime/debug"
"strconv" "strconv"
"time" "time"
) )
@@ -33,16 +36,16 @@ var (
) )
func handleInitClash(paramsString string) bool { func handleInitClash(paramsString string) bool {
runLock.Lock()
defer runLock.Unlock()
var params = InitParams{} var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params) err := json.Unmarshal([]byte(paramsString), &params)
if err != nil { if err != nil {
return false return false
} }
version = params.Version version = params.Version
if !isInit { constant.SetHomeDir(params.HomeDir)
constant.SetHomeDir(params.HomeDir) isInit = true
isInit = true
}
return isInit return isInit
} }
@@ -69,32 +72,76 @@ func handleGetIsInit() bool {
} }
func handleForceGC() { func handleForceGC() {
go func() { log.Infoln("[APP] request force GC")
log.Infoln("[APP] request force GC") runtime.GC()
runtime.GC() if features.Android {
}() debug.FreeOSMemory()
}
} }
func handleShutdown() bool { func handleShutdown() bool {
stopListeners() stopListeners()
executor.Shutdown() executor.Shutdown()
runtime.GC() handleForceGC()
isInit = false isInit = false
return true return true
} }
func handleValidateConfig(bytes []byte) string { func handleValidateConfig(path string) string {
_, err := config.UnmarshalRawConfig(bytes) buf, err := readFile(path)
_, err = config.UnmarshalRawConfig(buf)
if err != nil { if err != nil {
return err.Error() return err.Error()
} }
return "" return ""
} }
func handleGetProxies() map[string]constant.Proxy { func handleGetProxies() ProxiesData {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
return tunnel.ProxiesWithProviders()
nameList := config.GetProxyNameList()
proxies := make(map[string]constant.Proxy)
for name, proxy := range tunnel.Proxies() {
proxies[name] = proxy
}
for _, p := range tunnel.Providers() {
for _, proxy := range p.Proxies() {
proxies[proxy.Name()] = proxy
}
}
hasGlobal := false
allNames := make([]string, 0, len(nameList)+1)
for _, name := range nameList {
if name == "GLOBAL" {
hasGlobal = true
}
p, ok := proxies[name]
if !ok || p == nil {
continue
}
switch p.Type() {
case constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance:
allNames = append(allNames, name)
default:
}
}
if !hasGlobal {
if p, ok := proxies["GLOBAL"]; ok && p != nil {
allNames = append([]string{"GLOBAL"}, allNames...)
}
}
return ProxiesData{
All: allNames,
Proxies: proxies,
}
} }
func handleChangeProxy(data string, fn func(string string)) { func handleChangeProxy(data string, fn func(string string)) {
@@ -137,7 +184,7 @@ func handleChangeProxy(data string, fn func(string string)) {
} }
func handleGetTraffic(onlyStatisticsProxy bool) string { func handleGetTraffic(onlyStatisticsProxy bool) string {
up, down := statistic.DefaultManager.Current(onlyStatisticsProxy) up, down := statistic.DefaultManager.NowTraffic(onlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -151,7 +198,7 @@ func handleGetTraffic(onlyStatisticsProxy bool) string {
} }
func handleGetTotalTraffic(onlyStatisticsProxy bool) string { func handleGetTotalTraffic(onlyStatisticsProxy bool) string {
up, down := statistic.DefaultManager.Total(onlyStatisticsProxy) up, down := statistic.DefaultManager.TotalTraffic(onlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -281,7 +328,9 @@ func handleGetExternalProviders() string {
} }
eps = append(eps, *externalProvider) eps = append(eps, *externalProvider)
} }
sort.Sort(ExternalProviders(eps)) slices.SortFunc(eps, func(a, b ExternalProvider) int {
return cmp.Compare(a.Name, b.Name)
})
data, err := json.Marshal(eps) data, err := json.Marshal(eps)
if err != nil { if err != nil {
return "" return ""
@@ -483,14 +532,17 @@ func handleDelFile(path string, result ActionResult) {
} }
func handleSetupConfig(bytes []byte) string { func handleSetupConfig(bytes []byte) string {
if !isInit {
return "not initialized"
}
var params = defaultSetupParams() var params = defaultSetupParams()
err := UnmarshalJson(bytes, params) err := UnmarshalJson(bytes, params)
if err != nil { if err != nil {
log.Errorln("unmarshalRawConfig error %v", err) log.Errorln("unmarshalRawConfig error %v", err)
_ = setupConfig(defaultSetupParams()) _ = applyConfig(defaultSetupParams())
return err.Error() return err.Error()
} }
err = setupConfig(params) err = applyConfig(params)
if err != nil { if err != nil {
return err.Error() return err.Error()
} }

View File

@@ -37,6 +37,8 @@ type TunHandler struct {
} }
func (th *TunHandler) start(fd int, stack, address, dns string) { func (th *TunHandler) start(fd int, stack, address, dns string) {
runLock.Lock()
defer runLock.Unlock()
_ = th.limit.Acquire(context.TODO(), 4) _ = th.limit.Acquire(context.TODO(), 4)
defer th.limit.Release(4) defer th.limit.Release(4)
th.initHook() th.initHook()
@@ -199,9 +201,29 @@ func invokeAction(callback unsafe.Pointer, paramsChar *C.char) {
//export startTUN //export startTUN
func startTUN(callback unsafe.Pointer, fd C.int, stackChar, addressChar, dnsChar *C.char) bool { func startTUN(callback unsafe.Pointer, fd C.int, stackChar, addressChar, dnsChar *C.char) bool {
handleStartTun(callback, int(fd), takeCString(stackChar), takeCString(addressChar), takeCString(dnsChar)) handleStartTun(callback, int(fd), takeCString(stackChar), takeCString(addressChar), takeCString(dnsChar))
if !isRunning {
handleStartListener()
} else {
handleResetConnections()
}
return true return true
} }
//export quickSetup
func quickSetup(callback unsafe.Pointer, initParamsChar *C.char, setupParamsChar *C.char) {
go func() {
initParamsString := takeCString(initParamsChar)
setupParamsString := takeCString(setupParamsChar)
if !handleInitClash(initParamsString) {
invokeResult(callback, "init failed")
return
}
isRunning = true
message := handleSetupConfig([]byte(setupParamsString))
invokeResult(callback, message)
}()
}
//export setEventListener //export setEventListener
func setEventListener(listener unsafe.Pointer) { func setEventListener(listener unsafe.Pointer) {
if eventListener != nil || listener == nil { if eventListener != nil || listener == nil {
@@ -239,6 +261,9 @@ func sendMessage(message Message) {
//export stopTun //export stopTun
func stopTun() { func stopTun() {
handleStopTun() handleStopTun()
if isRunning {
handleStopListener()
}
} }
//export suspend //export suspend

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -24,15 +25,15 @@ class Application extends ConsumerStatefulWidget {
} }
class ApplicationState extends ConsumerState<Application> { class ApplicationState extends ConsumerState<Application> {
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer; Timer? _autoUpdateProfilesTaskTimer;
bool _preHasVpn = false;
final _pageTransitionsTheme = const PageTransitionsTheme( final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{ builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: CommonPageTransitionsBuilder(), TargetPlatform.android: commonSharedXPageTransitions,
TargetPlatform.windows: CommonPageTransitionsBuilder(), TargetPlatform.windows: commonSharedXPageTransitions,
TargetPlatform.linux: CommonPageTransitionsBuilder(), TargetPlatform.linux: commonSharedXPageTransitions,
TargetPlatform.macOS: CommonPageTransitionsBuilder(), TargetPlatform.macOS: commonSharedXPageTransitions,
}, },
); );
@@ -46,22 +47,22 @@ class ApplicationState extends ConsumerState<Application> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_autoUpdateProfilesTask();
globalState.appController = AppController(context, ref);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext; final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) { if (currentContext != null) {
globalState.appController = AppController(currentContext, ref); await appController.attach(currentContext, ref);
} else {
exit(0);
} }
await globalState.appController.init(); _autoUpdateProfilesTask();
globalState.appController.initLink(); appController.initLink();
app?.initShortcuts(); app?.initShortcuts();
}); });
} }
void _autoUpdateProfilesTask() { void _autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async { _autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles(); await appController.autoUpdateProfiles();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
}); });
} }
@@ -82,11 +83,13 @@ class ApplicationState extends ConsumerState<Application> {
child: CoreManager( child: CoreManager(
child: ConnectivityManager( child: ConnectivityManager(
onConnectivityChanged: (results) async { onConnectivityChanged: (results) async {
if (!results.contains(ConnectivityResult.vpn)) { commonPrint.log('connectivityChanged ${results.toString()}');
coreController.closeConnections(); appController.updateLocalIp();
final hasVpn = results.contains(ConnectivityResult.vpn);
if (_preHasVpn == hasVpn) {
appController.addCheckIp();
} }
globalState.appController.updateLocalIp(); _preHasVpn = hasVpn;
globalState.appController.addCheckIpNumDebounce();
}, },
child: child, child: child,
), ),
@@ -102,7 +105,7 @@ class ApplicationState extends ConsumerState<Application> {
} }
Widget _buildApp({required Widget child}) { Widget _buildApp({required Widget child}) {
return MessageManager(child: ThemeManager(child: child)); return StatusManager(child: ThemeManager(child: child));
} }
@override @override
@@ -162,11 +165,9 @@ class ApplicationState extends ConsumerState<Application> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
linkManager.destroy(); linkManager.destroy();
_autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel(); _autoUpdateProfilesTaskTimer?.cancel();
await coreController.destroy(); await coreController.destroy();
await globalState.appController.savePreferences(); await appController.handleExit();
await globalState.appController.handleExit();
super.dispose(); super.dispose();
} }
} }

View File

@@ -1,3 +1,3 @@
import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
final appLocalizations = AppLocalizations.current; final appLocalizations = AppLocalizations.current;

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:archive/archive_io.dart'; import 'package:archive/archive_io.dart';
@@ -7,6 +6,9 @@ import 'package:path/path.dart';
extension ArchiveExt on Archive { extension ArchiveExt on Archive {
void addDirectoryToArchive(String dirPath, String parentPath) { void addDirectoryToArchive(String dirPath, String parentPath) {
final dir = Directory(dirPath); final dir = Directory(dirPath);
if (!dir.existsSync()) {
return;
}
final entities = dir.listSync(recursive: false); final entities = dir.listSync(recursive: false);
for (final entity in entities) { for (final entity in entities) {
final relativePath = relative(entity.path, from: parentPath); final relativePath = relative(entity.path, from: parentPath);
@@ -15,14 +17,11 @@ extension ArchiveExt on Archive {
final archiveFile = ArchiveFile(relativePath, data.length, data); final archiveFile = ArchiveFile(relativePath, data.length, data);
addFile(archiveFile); addFile(archiveFile);
} }
// else if (entity is Directory) {
// addDirectoryToArchive(entity.path, parentPath);
// }
} }
} }
void addTextFile<T>(String name, T raw) { // void addTextFile<T>(String name, T raw) {
final data = json.encode(raw); // final data = json.encode(raw);
addFile(ArchiveFile.string(name, data)); // addFile(ArchiveFile.string(name, data));
} // }
} }

View File

@@ -6,17 +6,20 @@ export 'constant.dart';
export 'context.dart'; export 'context.dart';
export 'converter.dart'; export 'converter.dart';
export 'datetime.dart'; export 'datetime.dart';
export 'file.dart';
export 'fixed.dart'; export 'fixed.dart';
export 'function.dart'; export 'function.dart';
export 'future.dart'; export 'future.dart';
export 'http.dart'; export 'http.dart';
export 'icons.dart'; export 'icons.dart';
export 'indexing.dart';
export 'iterable.dart'; export 'iterable.dart';
export 'keyboard.dart'; export 'keyboard.dart';
export 'launch.dart'; export 'launch.dart';
export 'link.dart'; export 'link.dart';
export 'lock.dart'; export 'lock.dart';
export 'measure.dart'; export 'measure.dart';
export 'migration.dart';
export 'mixin.dart'; export 'mixin.dart';
export 'navigation.dart'; export 'navigation.dart';
export 'navigator.dart'; export 'navigator.dart';
@@ -32,9 +35,12 @@ export 'proxy.dart';
export 'render.dart'; export 'render.dart';
export 'request.dart'; export 'request.dart';
export 'scroll.dart'; export 'scroll.dart';
export 'snowflake.dart';
export 'string.dart'; export 'string.dart';
export 'system.dart'; export 'system.dart';
export 'task.dart';
export 'text.dart'; export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'utils.dart'; export 'utils.dart';
export 'window.dart'; export 'window.dart';
export 'yaml.dart';

View File

@@ -1,65 +1,65 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'string.dart';
List<Group> computeSort({ List<Group> computeSort({
required List<Group> groups, required List<Group> groups,
required ProxiesSortType sortType, required ProxiesSortType sortType,
required DelayMap delayMap, required DelayMap delayMap,
required SelectedMap selectedMap, required Map<String, String> selectedMap,
required String defaultTestUrl, required String defaultTestUrl,
}) { }) {
List<Proxy> sortOfDelay({
required List<Group> groups,
required List<Proxy> proxies,
required DelayMap delayMap,
required Map<String, String> selectedMap,
required String testUrl,
}) {
return List.from(proxies)..sort((a, b) {
final aDelayState = computeProxyDelayState(
proxyName: a.name,
testUrl: testUrl,
groups: groups,
selectedMap: selectedMap,
delayMap: delayMap,
);
final bDelayState = computeProxyDelayState(
proxyName: b.name,
testUrl: testUrl,
groups: groups,
selectedMap: selectedMap,
delayMap: delayMap,
);
return aDelayState.compareTo(bDelayState);
});
}
List<Proxy> sortOfName(List<Proxy> proxies) {
return List.of(proxies)..sort((a, b) => a.name.compareTo(b.name));
}
return groups.map((group) { return groups.map((group) {
final proxies = group.all; final proxies = group.all;
final newProxies = switch (sortType) { final newProxies = switch (sortType) {
ProxiesSortType.none => proxies, ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay( ProxiesSortType.delay => sortOfDelay(
groups: groups, groups: groups,
proxies: proxies, proxies: proxies,
delayMap: delayMap, delayMap: delayMap,
selectedMap: selectedMap, selectedMap: selectedMap,
testUrl: group.testUrl.getSafeValue(defaultTestUrl), testUrl: group.testUrl.takeFirstValid([defaultTestUrl]),
), ),
ProxiesSortType.name => _sortOfName(proxies), ProxiesSortType.name => sortOfName(proxies),
}; };
return group.copyWith(all: newProxies); return group.copyWith(all: newProxies);
}).toList(); }).toList();
} }
DelayState computeProxyDelayState({ SelectedProxyState getRealSelectedProxyState(
required String proxyName,
required String testUrl,
required List<Group> groups,
required SelectedMap selectedMap,
required DelayMap delayMap,
}) {
final state = computeRealSelectedProxyState(
proxyName,
groups: groups,
selectedMap: selectedMap,
);
final currentDelayMap = delayMap[state.testUrl.getSafeValue(testUrl)] ?? {};
final delay = currentDelayMap[state.proxyName];
return DelayState(delay: delay ?? 0, group: state.group);
}
SelectedProxyState computeRealSelectedProxyState(
String proxyName, {
required List<Group> groups,
required SelectedMap selectedMap,
}) {
return _getRealSelectedProxyState(
SelectedProxyState(proxyName: proxyName),
groups: groups,
selectedMap: selectedMap,
);
}
SelectedProxyState _getRealSelectedProxyState(
SelectedProxyState state, { SelectedProxyState state, {
required List<Group> groups, required List<Group> groups,
required SelectedMap selectedMap, required Map<String, String> selectedMap,
}) { }) {
if (state.proxyName.isEmpty) return state; if (state.proxyName.isEmpty) return state;
final index = groups.indexWhere((element) => element.name == state.proxyName); final index = groups.indexWhere((element) => element.name == state.proxyName);
@@ -72,39 +72,39 @@ SelectedProxyState _getRealSelectedProxyState(
if (currentSelectedName.isEmpty) { if (currentSelectedName.isEmpty) {
return newState; return newState;
} }
return _getRealSelectedProxyState( return getRealSelectedProxyState(
newState.copyWith(proxyName: currentSelectedName, testUrl: group.testUrl), newState.copyWith(proxyName: currentSelectedName, testUrl: group.testUrl),
groups: groups, groups: groups,
selectedMap: selectedMap, selectedMap: selectedMap,
); );
} }
List<Proxy> _sortOfDelay({ SelectedProxyState computeRealSelectedProxyState(
String proxyName, {
required List<Group> groups, required List<Group> groups,
required List<Proxy> proxies, required Map<String, String> selectedMap,
required DelayMap delayMap,
required SelectedMap selectedMap,
required String testUrl,
}) { }) {
return List.from(proxies)..sort((a, b) { return getRealSelectedProxyState(
final aDelayState = computeProxyDelayState( SelectedProxyState(proxyName: proxyName),
proxyName: a.name, groups: groups,
testUrl: testUrl, selectedMap: selectedMap,
groups: groups, );
selectedMap: selectedMap,
delayMap: delayMap,
);
final bDelayState = computeProxyDelayState(
proxyName: b.name,
testUrl: testUrl,
groups: groups,
selectedMap: selectedMap,
delayMap: delayMap,
);
return aDelayState.compareTo(bDelayState);
});
} }
List<Proxy> _sortOfName(List<Proxy> proxies) { DelayState computeProxyDelayState({
return List.of(proxies)..sort((a, b) => a.name.compareTo(b.name)); required String proxyName,
required String testUrl,
required List<Group> groups,
required Map<String, String> selectedMap,
required DelayMap delayMap,
}) {
final state = computeRealSelectedProxyState(
proxyName,
groups: groups,
selectedMap: selectedMap,
);
final currentDelayMap =
delayMap[state.testUrl.takeFirstValid([testUrl])] ?? {};
final delay = currentDelayMap[state.proxyName];
return DelayState(delay: delay ?? 0, group: state.group);
} }

View File

@@ -20,9 +20,18 @@ const helperPort = 47890;
const maxTextScale = 1.4; const maxTextScale = 1.4;
const minTextScale = 0.8; const minTextScale = 0.8;
final baseInfoEdgeInsets = EdgeInsets.symmetric( final baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16.ap, vertical: 16.mAp,
horizontal: 16.ap, horizontal: 16.mAp,
); );
final listHeaderPadding = EdgeInsets.only(
left: 16.mAp,
right: 8.mAp,
top: 24.mAp,
bottom: 8.mAp,
);
const sheetAppBarHeight = 68.0;
const watchExecution = true;
final defaultTextScaleFactor = final defaultTextScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor; WidgetsBinding.instance.platformDispatcher.textScaleFactor;
@@ -54,21 +63,30 @@ const defaultTestUrl = 'https://www.gstatic.com/generate_204';
final commonFilter = ImageFilter.blur( final commonFilter = ImageFilter.blur(
sigmaX: 5, sigmaX: 5,
sigmaY: 5, sigmaY: 5,
tileMode: TileMode.mirror, tileMode: TileMode.clamp,
); );
const listEquality = ListEquality();
const navigationItemListEquality = ListEquality<NavigationItem>(); const navigationItemListEquality = ListEquality<NavigationItem>();
const trackerInfoListEquality = ListEquality<TrackerInfo>(); const trackerInfoListEquality = ListEquality<TrackerInfo>();
const stringListEquality = ListEquality<String>(); const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>(); const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>(); const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>(); const groupListEquality = ListEquality<Group>();
const ruleListEquality = ListEquality<Rule>();
const scriptListEquality = ListEquality<Script>();
const externalProviderListEquality = ListEquality<ExternalProvider>(); const externalProviderListEquality = ListEquality<ExternalProvider>();
const packageListEquality = ListEquality<Package>(); const packageListEquality = ListEquality<Package>();
const profileListEquality = ListEquality<Profile>();
const proxyGroupsEquality = ListEquality<ProxyGroup>();
const hotKeyActionListEquality = ListEquality<HotKeyAction>(); const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>(); const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryListEquality =
ListEquality<MapEntry<String, String>>();
const stringAndStringMapEntryIterableEquality = const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>(); IterableEquality<MapEntry<String, String>>();
const stringAndObjectMapEntryIterableEquality =
IterableEquality<MapEntry<String, Object?>>();
const delayMapEquality = MapEquality<String, Map<String, int?>>(); const delayMapEquality = MapEquality<String, Map<String, int?>>();
const stringSetEquality = SetEquality<String>(); const stringSetEquality = SetEquality<String>();
const keyboardModifierListEquality = SetEquality<KeyboardModifier>(); const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
@@ -86,7 +104,8 @@ const profilesStoreKey = PageStorageKey<String>('profiles');
const defaultPrimaryColor = 0XFFD8C0C3; const defaultPrimaryColor = 0XFFD8C0C3;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 80 + (lines - 1) * 16, 0).ap; final space = 14.mAp;
return max(lines * (80.ap + space) - space, 0);
} }
const maxLength = 1000; const maxLength = 1000;
@@ -109,3 +128,6 @@ const scriptTemplate = '''
const main = (config) => { const main = (config) => {
return config; return config;
}'''; }''';
const backupDatabaseName = 'database.sqlite';
const configJsonName = 'config.json';

View File

@@ -1,5 +1,10 @@
import 'package:fl_clash/manager/message_manager.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/models/state.dart';
import 'package:fl_clash/widgets/inherited.dart';
import 'package:fl_clash/widgets/scaffold.dart'; import 'package:fl_clash/widgets/scaffold.dart';
import 'package:fl_clash/widgets/sheet.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension BuildContextExtension on BuildContext { extension BuildContextExtension on BuildContext {
@@ -7,8 +12,20 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<CommonScaffoldState>(); return findAncestorStateOfType<CommonScaffoldState>();
} }
void showNotifier(String text) { double get sheetTopPadding {
return findAncestorStateOfType<MessageManagerState>()?.message(text); final sheetType = SheetProvider.of(this)!.type;
if (sheetType == SheetType.bottomSheet) {
return sheetAppBarHeight;
} else {
return 10;
}
}
void showNotifier(String text, {MessageActionState? actionState}) {
return findAncestorStateOfType<StatusManagerState>()?.message(
text,
actionState: actionState,
);
} }
void showSnackBar(String message, {SnackBarAction? action}) { void showSnackBar(String message, {SnackBarAction? action}) {
@@ -42,6 +59,8 @@ extension BuildContextExtension on BuildContext {
TextTheme get textTheme => Theme.of(this).textTheme; TextTheme get textTheme => Theme.of(this).textTheme;
AppLocalizations get appLocalizations => AppLocalizations.of(this);
T? findLastStateOfType<T extends State>() { T? findLastStateOfType<T extends State>() {
T? state; T? state;

View File

@@ -17,23 +17,25 @@ extension DateTimeExtension on DateTime {
final difference = currentDateTime.difference(this); final difference = currentDateTime.difference(this);
final days = difference.inDays; final days = difference.inDays;
if (days >= 365) { if (days >= 365) {
return '${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}'; final years = (days / 365).floor();
return appLocalizations.yearsAgo(years);
} }
if (days >= 30) { if (days >= 30) {
return '${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}'; final months = (days / 30).floor();
return appLocalizations.monthsAgo(months);
} }
if (days >= 1) { if (days >= 1) {
return '$days ${appLocalizations.days}${appLocalizations.ago}'; return appLocalizations.daysAgo(days);
} }
final hours = difference.inHours; final hours = difference.inHours;
if (hours >= 1) { if (hours >= 1) {
return '$hours ${appLocalizations.hours}${appLocalizations.ago}'; return appLocalizations.hoursAgo(hours);
} }
final minutes = difference.inMinutes; final minutes = difference.inMinutes;
if (minutes >= 1) { if (minutes >= 1) {
return '$minutes ${appLocalizations.minutes}${appLocalizations.ago}'; return appLocalizations.minutesAgo(minutes);
} }
return appLocalizations.just; return appLocalizations.justNow;
} }
String get show { String get show {

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
@@ -10,19 +9,10 @@ class DAVClient {
Completer<bool> pingCompleter = Completer(); Completer<bool> pingCompleter = Completer();
late String fileName; late String fileName;
DAVClient(DAV dav) { DAVClient(DAVProps dav) {
client = newClient( client = newClient(dav.uri, user: dav.user, password: dav.password);
dav.uri,
user: dav.user,
password: dav.password,
);
fileName = dav.fileName; fileName = dav.fileName;
client.setHeaders( client.setHeaders({'accept-charset': 'utf-8', 'Content-Type': 'text/xml'});
{
'accept-charset': 'utf-8',
'Content-Type': 'text/xml',
},
);
client.setConnectTimeout(8000); client.setConnectTimeout(8000);
client.setSendTimeout(60000); client.setSendTimeout(60000);
client.setReceiveTimeout(60000); client.setReceiveTimeout(60000);
@@ -42,15 +32,16 @@ class DAVClient {
String get backupFile => '$root/$fileName'; String get backupFile => '$root/$fileName';
Future<bool> backup(Uint8List data) async { Future<bool> backup(String localFilePath) async {
await client.mkdir(root); await client.mkdir(root);
await client.write(backupFile, data); await client.writeFromFile(localFilePath, backupFile);
return true; return true;
} }
Future<List<int>> recovery() async { Future<bool> restore() async {
await client.mkdir(root); await client.mkdir(root);
final data = await client.read(backupFile); final backupFilePath = await appPath.backupFilePath;
return data; await client.read2File(backupFile, backupFilePath);
return true;
} }
} }

38
lib/common/file.dart Normal file
View File

@@ -0,0 +1,38 @@
import 'dart:io';
extension FileExt on File {
Future<void> safeCopy(String newPath) async {
if (!await exists()) {
await create(recursive: true);
return;
}
final targetFile = File(newPath);
if (!await targetFile.exists()) {
await targetFile.create(recursive: true);
}
await copy(newPath);
}
Future<File> safeWriteAsString(String str) async {
if (!await exists()) {
await create(recursive: true);
}
return await writeAsString(str);
}
Future<File> safeWriteAsBytes(List<int> bytes) async {
if (!await exists()) {
await create(recursive: true);
}
return await writeAsBytes(bytes);
}
}
extension FileSystemEntityExt on FileSystemEntity {
Future<void> safeDelete({bool recursive = false}) async {
if (!await exists()) {
return;
}
await delete(recursive: recursive);
}
}

View File

@@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/common/common.dart';
class Debouncer { class Debouncer {
final Map<FunctionTag, Timer?> _operations = {}; final Map<dynamic, Timer?> _operations = {};
void call( void call(
FunctionTag tag, dynamic tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration? duration, Duration? duration,
@@ -29,10 +29,10 @@ class Debouncer {
} }
class Throttler { class Throttler {
final Map<FunctionTag, Timer?> _operations = {}; final Map<dynamic, Timer?> _operations = {};
bool call( bool call(
FunctionTag tag, dynamic tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
@@ -68,7 +68,7 @@ Future<T> retry<T>({
required Future<T> Function() task, required Future<T> Function() task,
int maxAttempts = 3, int maxAttempts = 3,
required bool Function(T res) retryIf, required bool Function(T res) retryIf,
Duration delay = Duration.zero, Duration delay = midDuration,
}) async { }) async {
int attempts = 0; int attempts = 0;
while (attempts < maxAttempts) { while (attempts < maxAttempts) {
@@ -78,7 +78,7 @@ Future<T> retry<T>({
} }
attempts++; attempts++;
} }
throw 'unknown error'; throw 'retry error';
} }
final debouncer = Debouncer(); final debouncer = Debouncer();

View File

@@ -10,14 +10,14 @@ extension FutureExt<T> on Future<T> {
VoidCallback? onLast, VoidCallback? onLast,
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,
}) { }) {
final realTimout = timeout ?? const Duration(minutes: 3); final realTimeout = timeout ?? const Duration(minutes: 3);
Timer(realTimout + commonDuration, () { Timer(realTimeout + commonDuration, () {
if (onLast != null) { if (onLast != null) {
onLast(); onLast();
} }
}); });
return this.timeout( return this.timeout(
realTimout, realTimeout,
onTimeout: () async { onTimeout: () async {
if (onTimeout != null) { if (onTimeout != null) {
return onTimeout(); return onTimeout();

View File

@@ -1,15 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/controller.dart';
class FlClashHttpOverrides extends HttpOverrides { class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) { static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) { if ([localhost].contains(url.host)) {
return 'DIRECT'; return 'DIRECT';
} }
final port = globalState.config.patchClashConfig.mixedPort; final port = appController.config.patchClashConfig.mixedPort;
final isStart = globalState.appState.runTime != null; final isStart = appController.isStart;
commonPrint.log('find $url proxy:$isStart'); commonPrint.log('find $url proxy:$isStart');
if (!isStart) return 'DIRECT'; if (!isStart) return 'DIRECT';
return 'PROXY localhost:$port'; return 'PROXY localhost:$port';

264
lib/common/indexing.dart Normal file
View File

@@ -0,0 +1,264 @@
import 'dart:math';
class Indexing {
static const String digits =
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
static const String integerZero = 'a0';
static const String smallestInteger = 'A00000000000000000000000000';
static Indexing? _instance;
Indexing._internal();
factory Indexing() {
_instance ??= Indexing._internal();
return _instance!;
}
int _getIntegerLength(String head) {
if (head.compareTo('a') >= 0 && head.compareTo('z') <= 0) {
return head.codeUnitAt(0) - 'a'.codeUnitAt(0) + 2;
} else if (head.compareTo('A') >= 0 && head.compareTo('Z') <= 0) {
return 'Z'.codeUnitAt(0) - head.codeUnitAt(0) + 2;
} else {
throw Exception('Invalid order key head: $head');
}
}
bool _validateInteger(String integer) {
if (integer.length != _getIntegerLength(integer[0])) {
throw Exception('Invalid integer part of order key: $integer');
}
return true;
}
String? _incrementInteger(String x) {
_validateInteger(x);
String head = x[0];
List<String> digs = x.substring(1).split('');
bool carry = true;
for (int i = digs.length - 1; carry && i >= 0; i--) {
int d = digits.indexOf(digs[i]) + 1;
if (d == digits.length) {
digs[i] = '0';
} else {
digs[i] = digits[d];
carry = false;
}
}
if (carry) {
if (head == 'Z') {
return 'a0';
}
if (head == 'z') {
return null;
}
String h = String.fromCharCode(head.codeUnitAt(0) + 1);
if (h.compareTo('a') > 0) {
digs.add('0');
} else {
digs.removeLast();
}
return h + digs.join('');
} else {
return head + digs.join('');
}
}
String? _decrementInteger(String x) {
_validateInteger(x);
String head = x[0];
List<String> digs = x.substring(1).split('');
bool borrow = true;
for (int i = digs.length - 1; borrow && i >= 0; i--) {
int d = digits.indexOf(digs[i]) - 1;
if (d == -1) {
digs[i] = digits[digits.length - 1];
} else {
digs[i] = digits[d];
borrow = false;
}
}
if (borrow) {
if (head == 'a') {
return 'Z${digits[digits.length - 1]}';
}
if (head == 'A') {
return null;
}
String h = String.fromCharCode(head.codeUnitAt(0) - 1);
if (h.compareTo('Z') < 0) {
digs.add(digits[digits.length - 1]);
} else {
digs.removeLast();
}
return h + digs.join('');
} else {
return head + digs.join('');
}
}
String _midpoint(String a, String? b) {
if (b != null && a.compareTo(b) >= 0) {
throw Exception(
'Second order key must be greater than the first: $a, $b',
);
}
if (a.isNotEmpty && a[a.length - 1] == '0' ||
(b != null && b.isNotEmpty && b[b.length - 1] == '0')) {
throw Exception('Trailing zeros are not allowed: $a, $b');
}
if (b != null) {
int n = 0;
while ((n < a.length ? a[n] : '0') == b[n]) {
n++;
}
if (n > 0) {
return b.substring(0, n) +
_midpoint(
a.substring(min(n, a.length)),
b.substring(min(n, b.length)),
);
}
}
int digitA = (a.isNotEmpty) ? digits.indexOf(a[0]) : 0;
int digitB = (b != null && b.isNotEmpty)
? digits.indexOf(b[0])
: digits.length;
if (digitB - digitA > 1) {
int midDigit = (digitA + digitB + 1) ~/ 2;
return digits[midDigit];
} else {
if (b != null && b.length > 1) {
return b.substring(0, 1);
} else {
return digits[digitA] +
_midpoint(a.isNotEmpty ? a.substring(1) : '', null);
}
}
}
String _getIntegerPart(String key) {
int integerPartLength = _getIntegerLength(key[0]);
if (integerPartLength > key.length) {
throw Exception('Invalid order key: $key');
}
return key.substring(0, integerPartLength);
}
bool _validateOrderKey(String key) {
if (key == smallestInteger) {
throw Exception('Invalid order key: $key');
}
String i = _getIntegerPart(key);
String f = key.substring(i.length);
if (f.isNotEmpty && f[f.length - 1] == '0') {
throw Exception('Invalid order key: $key');
}
return true;
}
String? generateKeyBetween(String? a, String? b) {
if (a != null) {
_validateOrderKey(a);
}
if (b != null) {
_validateOrderKey(b);
}
if (a != null && b != null && a.compareTo(b) >= 0) {
throw Exception(
'Second order key must be greater than the first: $a, $b',
);
}
if (a == null && b == null) {
return integerZero;
}
if (a == null) {
b = b!;
String ib = _getIntegerPart(b);
String fb = b.substring(ib.length);
if (ib == smallestInteger) {
return ib + _midpoint('', fb);
}
return ib.compareTo(b) < 0 ? ib : _decrementInteger(ib);
}
if (b == null) {
String ia = _getIntegerPart(a);
String fa = a.substring(ia.length);
String? i = _incrementInteger(ia);
return i ?? ia + _midpoint(fa, null);
}
String ia = _getIntegerPart(a);
String fa = a.substring(ia.length);
String ib = _getIntegerPart(b);
String fb = b.substring(ib.length);
if (ia == ib) {
return ia + _midpoint(fa, fb);
}
String? i = _incrementInteger(ia);
return (i == null || i.compareTo(b) < 0) ? i : ia + _midpoint(fa, null);
}
List<String?> generateNKeysBetween(String? a, String? b, int n) {
if (n <= 0) {
return [];
}
if (n == 1) {
return [generateKeyBetween(a, b)];
}
if (b == null) {
String? c = generateKeyBetween(a, b);
List<String?> result = [c];
for (int i = 1; i < n; i++) {
c = generateKeyBetween(c, b);
result.add(c);
}
return result;
}
if (a == null) {
String? c = generateKeyBetween(a, b);
List<String?> result = [c];
for (int i = 1; i < n; i++) {
c = generateKeyBetween(a, c);
result.add(c);
}
return result.reversed.toList();
}
int mid = n ~/ 2;
String? c = generateKeyBetween(a, b);
return generateNKeysBetween(a, c, mid)
.followedBy([c])
.followedBy(generateNKeysBetween(c, b, n - mid - 1))
.toList();
}
List<String?> generateNKeys(int n) {
return generateNKeysBetween(null, null, n);
}
}
final indexing = Indexing();

View File

@@ -1,5 +1,5 @@
extension IterableExt<T> on Iterable<T> { extension IterableExt<E> on Iterable<E> {
Iterable<T> separated(T separator) sync* { Iterable<E> separated(E separator) sync* {
final iterator = this.iterator; final iterator = this.iterator;
if (!iterator.moveNext()) return; if (!iterator.moveNext()) return;
@@ -11,7 +11,7 @@ extension IterableExt<T> on Iterable<T> {
} }
} }
Iterable<List<T>> chunks(int size) sync* { Iterable<List<E>> chunks(int size) sync* {
if (length == 0) return; if (length == 0) return;
var iterator = this.iterator; var iterator = this.iterator;
while (iterator.moveNext()) { while (iterator.moveNext()) {
@@ -23,10 +23,7 @@ extension IterableExt<T> on Iterable<T> {
} }
} }
Iterable<T> fill( Iterable<E> fill(int length, {required E Function(int count) filler}) sync* {
int length, {
required T Function(int count) filler,
}) sync* {
int count = 0; int count = 0;
for (var item in this) { for (var item in this) {
yield item; yield item;
@@ -39,7 +36,7 @@ extension IterableExt<T> on Iterable<T> {
} }
} }
Iterable<T> takeLast({int count = 50}) { Iterable<E> takeLast({int count = 50}) {
if (count <= 0) return Iterable.empty(); if (count <= 0) return Iterable.empty();
return count >= length ? this : toList().skip(length - count); return count >= length ? this : toList().skip(length - count);
} }
@@ -81,9 +78,36 @@ extension ListExt<T> on List<T> {
return sublist(start); return sublist(start);
} }
T safeGet(int index) { T? safeGet(int index, {T? defaultValue}) {
if (length > index) return this[index]; if (index < 0 || index >= length) {
return last; return defaultValue;
}
return this[index];
}
T safeLast(T defaultValue) {
if (isNotEmpty) {
return last;
}
return defaultValue;
}
void addOrRemove(T value) {
if (contains(value)) {
remove(value);
} else {
add(value);
}
}
}
extension SetExt<T> on Set<T> {
void addOrRemove(T value) {
if (contains(value)) {
remove(value);
} else {
add(value);
}
} }
} }
@@ -120,4 +144,14 @@ extension MapExt<K, V> on Map<K, V> {
} }
return this[key]!; return this[key]!;
} }
Map<K, V> copyWitUpdate(K key, V? value) {
final newMap = Map<K, V>.from(this);
if (value == null) {
newMap.remove(key);
} else {
newMap[key] = value;
}
return newMap;
}
} }

View File

@@ -8,37 +8,33 @@ class Measure {
final Map<String, dynamic> _measureMap; final Map<String, dynamic> _measureMap;
Measure.of(this.context, double textScaleFactor) Measure.of(this.context, double textScaleFactor)
: _measureMap = {}, : _measureMap = {},
_textScaler = TextScaler.linear( _textScaler = TextScaler.linear(textScaleFactor);
textScaleFactor,
);
Size computeTextSize( TextPainter computeText(Text text, {TextStyle? style, double? maxWidth}) {
Text text, { return TextPainter(
double maxWidth = double.infinity, text: TextSpan(text: text.data, style: text.style ?? style),
}) {
final textPainter = TextPainter(
text: TextSpan(
text: text.data,
style: text.style,
),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScaler, textScaler: _textScaler,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout( )..layout(maxWidth: maxWidth ?? double.infinity);
maxWidth: maxWidth, }
);
Size computeTextSize(Text text, {TextStyle? style, double? maxWidth}) {
final textPainter = computeText(text, style: style, maxWidth: maxWidth);
return textPainter.size; return textPainter.size;
} }
bool computeTextIsOverflow(Text text, {TextStyle? style, double? maxWidth}) {
final textPainter = computeText(text, style: style, maxWidth: maxWidth);
return textPainter.didExceedMaxLines;
}
double get bodyMediumHeight { double get bodyMediumHeight {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'bodyMediumHeight', 'bodyMediumHeight',
() => computeTextSize( () => computeTextSize(
Text( Text('X', style: context.textTheme.bodyMedium),
'X',
style: context.textTheme.bodyMedium,
),
).height, ).height,
); );
} }
@@ -46,24 +42,16 @@ class Measure {
double get bodyLargeHeight { double get bodyLargeHeight {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'bodyLargeHeight', 'bodyLargeHeight',
() => computeTextSize( () =>
Text( computeTextSize(Text('X', style: context.textTheme.bodyLarge)).height,
'X',
style: context.textTheme.bodyLarge,
),
).height,
); );
} }
double get bodySmallHeight { double get bodySmallHeight {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'bodySmallHeight', 'bodySmallHeight',
() => computeTextSize( () =>
Text( computeTextSize(Text('X', style: context.textTheme.bodySmall)).height,
'X',
style: context.textTheme.bodySmall,
),
).height,
); );
} }
@@ -71,10 +59,16 @@ class Measure {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'labelSmallHeight', 'labelSmallHeight',
() => computeTextSize( () => computeTextSize(
Text( Text('X', style: context.textTheme.labelSmall),
'X', ).height,
style: context.textTheme.labelSmall, );
), }
double get titleSmallHeight {
return _measureMap.updateCacheValue(
'titleSmallHeight',
() => computeTextSize(
Text('X', style: context.textTheme.titleSmall),
).height, ).height,
); );
} }
@@ -83,10 +77,7 @@ class Measure {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'labelMediumHeight', 'labelMediumHeight',
() => computeTextSize( () => computeTextSize(
Text( Text('X', style: context.textTheme.labelMedium),
'X',
style: context.textTheme.labelMedium,
),
).height, ).height,
); );
} }
@@ -95,10 +86,7 @@ class Measure {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'titleLargeHeight', 'titleLargeHeight',
() => computeTextSize( () => computeTextSize(
Text( Text('X', style: context.textTheme.titleLarge),
'X',
style: context.textTheme.titleLarge,
),
).height, ).height,
); );
} }
@@ -107,10 +95,7 @@ class Measure {
return _measureMap.updateCacheValue( return _measureMap.updateCacheValue(
'titleMediumHeight', 'titleMediumHeight',
() => computeTextSize( () => computeTextSize(
Text( Text('X', style: context.textTheme.titleMedium),
'X',
style: context.textTheme.titleMedium,
),
).height, ).height,
); );
} }

53
lib/common/migration.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
class Migration {
static Migration? _instance;
late int _oldVersion;
Migration._internal();
final currentVersion = 1;
factory Migration() {
_instance ??= Migration._internal();
return _instance!;
}
Future<Config> migrationIfNeeded(
Map<String, Object?>? configMap, {
required Future<Config> Function(MigrationData data) sync,
}) async {
_oldVersion = await preferences.getVersion();
if (_oldVersion == currentVersion) {
try {
return Config.realFromJson(configMap);
} catch (_) {
final isV0 = configMap?['proxiesStyle'] != null;
if (isV0) {
_oldVersion = 0;
} else {
throw 'Local data is damaged. A reset is required to fix this issue.';
}
}
}
MigrationData data = MigrationData(configMap: configMap);
if (_oldVersion == 0 && configMap != null) {
final clashConfigMap = await preferences.getClashConfigMap();
if (clashConfigMap != null) {
configMap['patchClashConfig'] = clashConfigMap;
await preferences.clearClashConfig();
}
data = await _oldToNow(configMap);
}
final res = await sync(data);
await preferences.setVersion(currentVersion);
return res;
}
Future<MigrationData> _oldToNow(Map<String, Object?> configMap) async {
return await oldToNowTask(configMap);
}
}
final migration = Migration();

View File

@@ -1,18 +1,25 @@
import 'package:flutter/cupertino.dart';
import 'package:riverpod/riverpod.dart'; import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'utils.dart';
mixin AutoDisposeNotifierMixin<T> on AnyNotifier<T, T> { mixin AutoDisposeNotifierMixin<T> on AnyNotifier<T, T> {
T get value => state;
set value(T value) { set value(T value) {
if (ref.mounted) { state = value;
state = value; }
} else {
onUpdate(value); bool equals(T previous, T next) {
} return false;
} }
@override @override
bool updateShouldNotify(previous, next) { bool updateShouldNotify(previous, next) {
final res = super.updateShouldNotify(previous, next); final res = !equals(previous, next)
? super.updateShouldNotify(previous, next)
: true;
if (res) { if (res) {
onUpdate(next); onUpdate(next);
} }
@@ -20,27 +27,24 @@ mixin AutoDisposeNotifierMixin<T> on AnyNotifier<T, T> {
} }
void onUpdate(T value) {} void onUpdate(T value) {}
void update(T? Function(T) builder) {
final res = builder(value);
if (res == null) {
return;
}
value = res;
}
} }
mixin AnyNotifierMixin<T> on AnyNotifier<T, T> { mixin AsyncNotifierMixin<T> on AnyNotifier<AsyncValue<T>, T> {
T get value; T get value;
set value(T value) { set value(T value) {
if (ref.mounted) { state = AsyncData(value);
state = value;
} else {
onUpdate(value);
}
} }
}
@override
bool updateShouldNotify(previous, next) { mixin UniqueKeyStateMixin<T extends StatefulWidget> on State<T> {
final res = super.updateShouldNotify(previous, next); final key = utils.id;
if (res) {
onUpdate(next);
}
return res;
}
void onUpdate(T value) {}
} }

View File

@@ -1,12 +1,11 @@
import 'package:animations/animations.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/controller.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BaseNavigator { class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async { static Future<T?> push<T>(BuildContext context, Widget child) async {
if (globalState.appState.viewMode != ViewMode.mobile) { if (!appController.isMobile) {
return await Navigator.of( return await Navigator.of(
context, context,
).push<T>(CommonDesktopRoute(builder: (context) => child)); ).push<T>(CommonDesktopRoute(builder: (context) => child));
@@ -32,6 +31,11 @@ class BaseNavigator {
// } // }
} }
const commonSharedXPageTransitions = SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
fillColor: Colors.transparent,
);
class CommonDesktopRoute<T> extends PageRoute<T> { class CommonDesktopRoute<T> extends PageRoute<T> {
final Widget Function(BuildContext context) builder; final Widget Function(BuildContext context) builder;
@@ -67,14 +71,45 @@ class CommonDesktopRoute<T> extends PageRoute<T> {
Duration get reverseTransitionDuration => Duration(milliseconds: 200); Duration get reverseTransitionDuration => Duration(milliseconds: 200);
} }
class CommonRoute<T> extends MaterialPageRoute<T> { class CommonRoute<T> extends PageRoute<T> {
CommonRoute({required super.builder}); final Widget Function(BuildContext context) builder;
CommonRoute({required this.builder});
@override @override
Duration get transitionDuration => const Duration(milliseconds: 500); Color? get barrierColor => null;
@override @override
Duration get reverseTransitionDuration => const Duration(milliseconds: 500); String? get barrierLabel => null;
@override
bool get maintainState => true;
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final Widget result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
fillColor: context.colorScheme.surface,
child: result,
),
);
}
@override
Duration get transitionDuration => Duration(milliseconds: 300);
@override
Duration get reverseTransitionDuration => Duration(milliseconds: 300);
} }
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -228,7 +263,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
DecorationTween( DecorationTween(
begin: const _CommonEdgeShadowDecoration(), begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration(<Color>[ end: _CommonEdgeShadowDecoration(<Color>[
widget.context.colorScheme.inverseSurface.withValues(alpha: 0.02), Color(0x04000000),
Colors.transparent, Colors.transparent,
]), ]),
), ),
@@ -279,7 +314,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return; return;
} }
final double shadowWidth = 1 * configuration.size!.width; final double shadowWidth = 0.05 * configuration.size!.width;
final double shadowHeight = configuration.size!.height; final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1); final double bandWidth = shadowWidth / (colors.length - 1);

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -20,6 +22,10 @@ extension NumExt on num {
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5); return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
} }
double get mAp {
return this * min((1 + (globalState.theme.textScaleFactor - 1) * 0.5), 1);
}
TrafficShow get traffic { TrafficShow get traffic {
final units = TrafficUnit.values; final units = TrafficUnit.values;
var size = toDouble(); var size = toDouble();
@@ -33,6 +39,20 @@ extension NumExt on num {
unit: units[unitIndex].name, unit: units[unitIndex].name,
); );
} }
TrafficShow get shortTraffic {
final units = TrafficUnit.values;
var size = toDouble();
var unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return TrafficShow(
value: size.toStringAsFixed(0),
unit: ' ${units[unitIndex].name}',
);
}
} }
extension DoubleExt on double { extension DoubleExt on double {

View File

@@ -61,14 +61,39 @@ class AppPath {
return directory.path; return directory.path;
} }
Future<String> get databasePath async {
final mHomeDirPath = await homeDirPath;
return join(mHomeDirPath, 'database.sqlite');
}
Future<String> get backupFilePath async {
final mHomeDirPath = await homeDirPath;
return join(mHomeDirPath, 'backup.zip');
}
Future<String> get restoreDirPath async {
final mHomeDirPath = await homeDirPath;
return join(mHomeDirPath, 'restore');
}
Future<String> get tempFilePath async {
final mTempDir = await tempDir.future;
return join(mTempDir.path, 'temp${utils.id}');
}
Future<String> get lockFilePath async { Future<String> get lockFilePath async {
final homeDirPath = await appPath.homeDirPath; final homeDirPath = await appPath.homeDirPath;
return join(homeDirPath, 'FlClash.lock'); return join(homeDirPath, 'FlClash.lock');
} }
Future<String> get configFilePath async { Future<String> get configFilePath async {
final homeDirPath = await appPath.homeDirPath; final mHomeDirPath = await homeDirPath;
return join(homeDirPath, 'config.json'); return join(mHomeDirPath, 'config.yaml');
}
Future<String> get sharedFilePath async {
final mHomeDirPath = await homeDirPath;
return join(mHomeDirPath, 'shared.json');
} }
Future<String> get sharedPreferencesPath async { Future<String> get sharedPreferencesPath async {
@@ -81,9 +106,18 @@ class AppPath {
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }
Future<String> getProfilePath(String id) async { Future<String> getProfilePath(String fileName) async {
final directory = await profilesPath; return join(await profilesPath, '$fileName.yaml');
return join(directory, '$id.yaml'); }
Future<String> get scriptsDirPath async {
final path = await homeDirPath;
return join(path, 'scripts');
}
Future<String> getScriptPath(String fileName) async {
final path = await scriptsDirPath;
return join(path, '$fileName.js');
} }
Future<String> getIconsCacheDir() async { Future<String> getIconsCacheDir() async {

View File

@@ -7,9 +7,9 @@ import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
class Picker { class Picker {
Future<PlatformFile?> pickerFile() async { Future<PlatformFile?> pickerFile({bool withData = true}) async {
final filePickerResult = await FilePicker.platform.pickFiles( final filePickerResult = await FilePicker.platform.pickFiles(
withData: true, withData: withData,
allowMultiple: false, allowMultiple: false,
initialDirectory: await appPath.downloadDirPath, initialDirectory: await appPath.downloadDirPath,
); );
@@ -20,15 +20,33 @@ class Picker {
final path = await FilePicker.platform.saveFile( final path = await FilePicker.platform.saveFile(
fileName: fileName, fileName: fileName,
initialDirectory: await appPath.downloadDirPath, initialDirectory: await appPath.downloadDirPath,
bytes: system.isAndroid ? bytes : null, bytes: bytes,
); );
if (!system.isAndroid && path != null) { if (!system.isAndroid && path != null) {
final file = await File(path).create(recursive: true); final file = File(path);
await file.writeAsBytes(bytes); await file.safeWriteAsBytes(bytes);
} }
return path; return path;
} }
Future<String?> saveFileWithPath(String fileName, String localPath) async {
final localFile = File(localPath);
if (!await localFile.exists()) {
await localFile.create(recursive: true);
}
final bytes = Platform.isAndroid ? await localFile.readAsBytes() : null;
final path = await FilePicker.platform.saveFile(
fileName: fileName,
initialDirectory: await appPath.downloadDirPath,
bytes: bytes,
);
if (path != null && bytes == null) {
await localFile.copy(path);
}
await localFile.safeDelete();
return path;
}
Future<String?> pickerConfigQRCode() async { Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery); final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
if (xFile == null) { if (xFile == null) {

View File

@@ -24,36 +24,70 @@ class Preferences {
return _instance!; return _instance!;
} }
Future<ClashConfig?> getClashConfig() async { Future<int> getVersion() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences?.getString(clashConfigKey); return preferences?.getInt('version') ?? 0;
if (clashConfigString == null) return null; }
final clashConfigMap = json.decode(clashConfigString);
return ClashConfig.fromJson(clashConfigMap); Future<void> setVersion(int version) async {
final preferences = await sharedPreferencesCompleter.future;
await preferences?.setInt('version', version);
}
Future<void> saveShareState(SharedState shareState) async {
final preferences = await sharedPreferencesCompleter.future;
await preferences?.setString('sharedState', json.encode(shareState));
}
Future<Map<String, Object?>?> getConfigMap() async {
try {
final preferences = await sharedPreferencesCompleter.future;
final configString = preferences?.getString(configKey);
if (configString == null) return null;
final Map<String, Object?>? configMap = json.decode(configString);
return configMap;
} catch (_) {
return null;
}
}
Future<Map<String, Object?>?> getClashConfigMap() async {
try {
final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences?.getString(clashConfigKey);
if (clashConfigString == null) return null;
return json.decode(clashConfigString);
} catch (_) {
return null;
}
}
Future<void> clearClashConfig() async {
try {
final preferences = await sharedPreferencesCompleter.future;
await preferences?.remove(clashConfigKey);
return;
} catch (_) {
return;
}
} }
Future<Config?> getConfig() async { Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future; final configMap = await getConfigMap();
final configString = preferences?.getString(configKey); if (configMap == null) {
if (configString == null) return null; return null;
final configMap = json.decode(configString); }
return Config.compatibleFromJson(configMap); return Config.fromJson(configMap);
} }
Future<bool> saveConfig(Config config) async { Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
return await preferences?.setString(configKey, json.encode(config)) ?? return preferences?.setString(configKey, json.encode(config)) ?? false;
false;
}
Future<void> clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
} }
Future<void> clearPreferences() async { Future<void> clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future; final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns?.clear(); await sharedPreferencesIns?.clear();
} }
} }

View File

@@ -1,7 +1,7 @@
import 'package:fl_clash/controller.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class CommonPrint { class CommonPrint {
static CommonPrint? _instance; static CommonPrint? _instance;
@@ -16,12 +16,10 @@ class CommonPrint {
void log(String? text, {LogLevel logLevel = LogLevel.info}) { void log(String? text, {LogLevel logLevel = LogLevel.info}) {
final payload = '[APP] $text'; final payload = '[APP] $text';
debugPrint(payload); debugPrint(payload);
if (!globalState.isInit) { if (!appController.isAttach) {
return; return;
} }
globalState.appController.addLog( appController.addLog(Log.app(payload).copyWith(logLevel: logLevel));
Log.app(payload).copyWith(logLevel: logLevel),
);
} }
} }

View File

@@ -6,6 +6,8 @@ import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/controller.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@@ -22,7 +24,7 @@ class Request {
createHttpClient: () { createHttpClient: () {
final client = HttpClient(); final client = HttpClient();
client.findProxy = (Uri uri) { client.findProxy = (Uri uri) {
client.userAgent = globalState.ua; client.userAgent = appController.ua;
return FlClashHttpOverrides.handleFindProxy(uri); return FlClashHttpOverrides.handleFindProxy(uri);
}; };
return client; return client;
@@ -30,16 +32,28 @@ class Request {
); );
} }
Future<Response> getFileResponseForUrl(String url) async { Future<Response<Uint8List>> getFileResponseForUrl(String url) async {
final response = await _clashDio.get( try {
url, return await _clashDio.get<Uint8List>(
options: Options(responseType: ResponseType.bytes), url,
); options: Options(responseType: ResponseType.bytes),
return response; );
} catch (e) {
commonPrint.log('getFileResponseForUrl error ${e.toString()}');
if (e is DioException) {
if (e.type == DioExceptionType.unknown) {
throw appLocalizations.unknownNetworkError;
} else if (e.type == DioExceptionType.badResponse) {
throw appLocalizations.networkException;
}
rethrow;
}
throw appLocalizations.unknownNetworkError;
}
} }
Future<Response> getTextResponseForUrl(String url) async { Future<Response<String>> getTextResponseForUrl(String url) async {
final response = await _clashDio.get( final response = await _clashDio.get<String>(
url, url,
options: Options(responseType: ResponseType.plain), options: Options(responseType: ResponseType.plain),
); );
@@ -58,18 +72,23 @@ class Request {
} }
Future<Map<String, dynamic>?> checkForUpdate() async { Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await dio.get( try {
'https://api.github.com/repos/$repository/releases/latest', final response = await dio.get(
options: Options(responseType: ResponseType.json), 'https://api.github.com/repos/$repository/releases/latest',
); options: Options(responseType: ResponseType.json),
if (response.statusCode != 200) return null; );
final data = response.data as Map<String, dynamic>; if (response.statusCode != 200) return null;
final remoteVersion = data['tag_name']; final data = response.data as Map<String, dynamic>;
final version = globalState.packageInfo.version; final remoteVersion = data['tag_name'];
final hasUpdate = final version = globalState.packageInfo.version;
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; final hasUpdate =
if (!hasUpdate) return null; utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
return data; if (!hasUpdate) return null;
return data;
} catch (e) {
commonPrint.log('checkForUpdate failed', logLevel: LogLevel.warning);
return null;
}
} }
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = { final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
@@ -84,6 +103,7 @@ class Request {
Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async { Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
var failureCount = 0; var failureCount = 0;
final token = cancelToken ?? CancelToken();
final futures = _ipInfoSources.entries.map((source) async { final futures = _ipInfoSources.entries.map((source) async {
final Completer<Result<IpInfo?>> completer = Completer(); final Completer<Result<IpInfo?>> completer = Completer();
handleFailRes() { handleFailRes() {
@@ -95,7 +115,7 @@ class Request {
final future = dio final future = dio
.get<Map<String, dynamic>>( .get<Map<String, dynamic>>(
source.key, source.key,
cancelToken: cancelToken, cancelToken: token,
options: Options(responseType: ResponseType.json), options: Options(responseType: ResponseType.json),
) )
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 10));
@@ -118,7 +138,7 @@ class Request {
return completer.future; return completer.future;
}); });
final res = await Future.any(futures); final res = await Future.any(futures);
cancelToken?.cancel(); token.cancel();
return res; return res;
} }

58
lib/common/snowflake.dart Normal file
View File

@@ -0,0 +1,58 @@
class Snowflake {
static Snowflake? _instance;
Snowflake._internal();
factory Snowflake() {
_instance ??= Snowflake._internal();
return _instance!;
}
static const int twepoch = 1704067200000;
static const int workerIdBits = 10;
static const int sequenceBits = 12;
static const int maxWorkerId = -1 ^ (-1 << workerIdBits);
static const int sequenceMask = -1 ^ (-1 << sequenceBits);
static const int workerIdShift = sequenceBits;
static const int timestampLeftShift = sequenceBits + workerIdBits;
final int workerId = 1;
int _lastTimestamp = -1;
int _sequence = 0;
int get id {
int timestamp = DateTime.now().millisecondsSinceEpoch;
if (timestamp < _lastTimestamp) {
throw ArgumentError(
'Clock moved backwards. Refusing to generate id for ${_lastTimestamp - timestamp} milliseconds',
);
}
if (timestamp == _lastTimestamp) {
_sequence = (_sequence + 1) & sequenceMask;
if (_sequence == 0) {
timestamp = _getNextMillis(_lastTimestamp);
}
} else {
_sequence = 0;
}
_lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(workerId << workerIdShift) |
_sequence;
}
int _getNextMillis(int lastTimestamp) {
int timestamp = DateTime.now().millisecondsSinceEpoch;
while (timestamp <= lastTimestamp) {
timestamp = DateTime.now().millisecondsSinceEpoch;
}
return timestamp;
}
}
final snowflake = Snowflake();

View File

33
lib/common/store.dart Normal file
View File

@@ -0,0 +1,33 @@
import 'dart:async';
class Store<T> {
late T _data;
Store(Stream stream, T defaultValue) {
stream.listen((data) {
_add(data);
});
_data = defaultValue;
}
bool equals(T oldValue, T newValue) {
return oldValue == newValue;
}
void _add(T value) {
if (!equals(_data, value)) {
_streamController.add(value);
_data = value;
}
}
final StreamController<T> _streamController = StreamController<T>.broadcast();
Stream<T> get stream => _streamController.stream;
T get value => _data;
set value(T value) {
_add(value);
}
}

View File

@@ -2,8 +2,7 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:fl_clash/common/common.dart';
import 'print.dart';
extension StringExtension on String { extension StringExtension on String {
bool get isUrl { bool get isUrl {
@@ -22,6 +21,16 @@ extension StringExtension on String {
return toLowerCase().compareTo(other.toLowerCase()); return toLowerCase().compareTo(other.toLowerCase());
} }
String safeSubstring(int start, [int? end]) {
if (isEmpty) return '';
final safeStart = start.clamp(0, length);
if (end == null) {
return substring(safeStart);
}
final safeEnd = end.clamp(safeStart, length);
return substring(safeStart, safeEnd);
}
List<int> get encodeUtf16LeWithBom { List<int> get encodeUtf16LeWithBom {
final byteData = ByteData(length * 2); final byteData = ByteData(length * 2);
final bom = [0xFF, 0xFE]; final bom = [0xFF, 0xFE];
@@ -68,13 +77,26 @@ extension StringExtension on String {
// bool containsToLower(String target) { // bool containsToLower(String target) {
// return toLowerCase().contains(target); // return toLowerCase().contains(target);
// } // }
}
extension StringExtensionSafe on String? { Future<T> commonToJSON<T>() async {
String getSafeValue(String defaultValue) { final thresholdLimit = 51200;
if (this == null || this!.isEmpty) { if (length < thresholdLimit) {
return defaultValue; return json.decode(this);
} else {
return await decodeJSONTask<T>(this);
} }
return this!; }
}
extension StringNullExt on String? {
String takeFirstValid(List<String?> others, {String defaultValue = ''}) {
if (this != null && this!.trim().isNotEmpty) return this!.trim();
for (final s in others) {
if (s != null && s.trim().isNotEmpty) {
return s.trim();
}
}
return defaultValue;
} }
} }

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