Compare commits
3 Commits
v0.8.91-pr
...
release/v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae248208b | ||
|
|
6e404ab19c | ||
|
|
2395a4b20c |
110
.github/workflows/build.yaml
vendored
@@ -22,7 +22,7 @@ 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
|
||||||
@@ -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 }}
|
||||||
|
|||||||
11
.gitignore
vendored
@@ -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,6 +41,11 @@ 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
|
||||||
@@ -53,7 +58,6 @@ app.*.map.json
|
|||||||
/android/core/**/cmake-build-*/
|
/android/core/**/cmake-build-*/
|
||||||
/android/core/**/jniLibs/
|
/android/core/**/jniLibs/
|
||||||
|
|
||||||
|
|
||||||
#FlClash
|
#FlClash
|
||||||
/libclash/
|
/libclash/
|
||||||
/android/app/src/main/jniLibs/
|
/android/app/src/main/jniLibs/
|
||||||
@@ -61,3 +65,6 @@ app.*.map.json
|
|||||||
/macos/**/Package.resolved
|
/macos/**/Package.resolved
|
||||||
devtools_options.yaml
|
devtools_options.yaml
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
|
.fvmrc
|
||||||
4
.gitmodules
vendored
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
29
.metadata
@@ -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
@@ -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>
|
||||||
10
CHANGELOG.md
@@ -1,3 +1,13 @@
|
|||||||
|
## v0.8.90
|
||||||
|
|
||||||
|
- Fix android tile service
|
||||||
|
|
||||||
|
- Support append system DNS
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
## v0.8.89
|
## v0.8.89
|
||||||
|
|
||||||
- Fix some issues
|
- Fix some issues
|
||||||
|
|||||||
10
Makefile
@@ -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
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -8,6 +9,7 @@ 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
|
||||||
@@ -40,7 +42,7 @@ 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(
|
||||||
@@ -51,13 +53,50 @@ object Service {
|
|||||||
res.add(result ?: byteArrayOf())
|
res.add(result ?: byteArrayOf())
|
||||||
ack?.onAck()
|
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> {
|
||||||
@@ -65,24 +104,24 @@ object Service {
|
|||||||
return delegate.useService {
|
return delegate.useService {
|
||||||
it.setEventListener(
|
it.setEventListener(
|
||||||
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, ack: IAckInterface?
|
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()
|
ack?.onAck()
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
cb(results[id]?.formatString())
|
cb(results[id]?.formatString())
|
||||||
results.remove(id)
|
results.remove(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false -> null
|
false -> null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 +50,17 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ object State {
|
|||||||
if (flutterEngine != null) {
|
if (flutterEngine != null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startServiceWithEngine()
|
startServiceWithPref()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -84,16 +84,18 @@ object State {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tilePlugin?.handleStop()
|
tilePlugin?.handleStop()
|
||||||
if (flutterEngine != null || serviceFlutterEngine != null) {
|
if (flutterEngine != null) {
|
||||||
return
|
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
|
||||||
@@ -101,75 +103,101 @@ object State {
|
|||||||
startService()
|
startService()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleStopService() {
|
private fun startServiceWithPref() {
|
||||||
GlobalState.launch {
|
|
||||||
runLock.withLock {
|
|
||||||
if (runStateFlow.value != RunState.START) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
runStateFlow.tryEmit(RunState.PENDING)
|
|
||||||
runTime = Service.stopService()
|
|
||||||
runStateFlow.tryEmit(RunState.STOP)
|
|
||||||
}
|
|
||||||
destroyServiceEngine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun destroyServiceEngine() {
|
|
||||||
runLock.withLock {
|
|
||||||
GlobalState.log("Destroy service engine")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
runCatching {
|
|
||||||
serviceFlutterEngine?.destroy()
|
|
||||||
serviceFlutterEngine = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startServiceWithEngine() {
|
|
||||||
GlobalState.launch {
|
GlobalState.launch {
|
||||||
runLock.withLock {
|
runLock.withLock {
|
||||||
if (runStateFlow.value != RunState.STOP) {
|
if (runStateFlow.value != RunState.STOP) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
GlobalState.log("Create service engine")
|
sharedState = GlobalState.application.sharedState
|
||||||
withContext(Dispatchers.Main) {
|
setupAndStart()
|
||||||
serviceFlutterEngine?.destroy()
|
|
||||||
serviceFlutterEngine = FlutterEngine(GlobalState.application)
|
|
||||||
serviceFlutterEngine?.plugins?.add(ServicePlugin())
|
|
||||||
serviceFlutterEngine?.plugins?.add(AppPlugin())
|
|
||||||
serviceFlutterEngine?.plugins?.add(TilePlugin())
|
|
||||||
val dartEntrypoint = DartExecutor.DartEntrypoint(
|
|
||||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(), "_service"
|
|
||||||
)
|
|
||||||
serviceFlutterEngine?.dartExecutor?.executeDartEntrypoint(dartEntrypoint)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun syncState() {
|
||||||
|
GlobalState.setCrashlytics(sharedState.crashlytics)
|
||||||
|
Service.updateNotificationParams(
|
||||||
|
NotificationParams(
|
||||||
|
title = sharedState.currentProfileName,
|
||||||
|
stopText = sharedState.stopText,
|
||||||
|
onlyStatisticsProxy = sharedState.onlyStatisticsProxy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Service.setCrashlytics(sharedState.crashlytics)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setupAndStart() {
|
||||||
|
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.STOP) {
|
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() {
|
||||||
|
GlobalState.launch {
|
||||||
|
runLock.withLock {
|
||||||
|
if (runStateFlow.value != RunState.START) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
runStateFlow.tryEmit(RunState.PENDING)
|
||||||
|
runTime = Service.stopService()
|
||||||
|
runStateFlow.tryEmit(RunState.STOP)
|
||||||
|
} finally {
|
||||||
|
if (runStateFlow.value == RunState.PENDING) {
|
||||||
|
runStateFlow.tryEmit(RunState.START)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// IVoidInterface.aidl
|
||||||
|
package com.follow.clash.service;
|
||||||
|
|
||||||
|
interface IVoidInterface {
|
||||||
|
oneway void invoke();
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -98,6 +98,35 @@ class RemoteService : Service(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -108,6 +137,7 @@ class RemoteService : Service(),
|
|||||||
runtime: Long,
|
runtime: Long,
|
||||||
result: IResultInterface,
|
result: IResultInterface,
|
||||||
) {
|
) {
|
||||||
|
GlobalState.log("remote startService")
|
||||||
State.options = options
|
State.options = options
|
||||||
handleStartService(runtime, result)
|
handleStartService(runtime, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
@@ -234,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -434,5 +428,53 @@
|
|||||||
"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",
|
"appendSystemDns": "Append System DNS",
|
||||||
"appendSystemDnsTip": "Forcefully append system DNS to the configuration"
|
"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"
|
||||||
}
|
}
|
||||||
@@ -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": "リネーム",
|
||||||
@@ -435,5 +429,53 @@
|
|||||||
"crashlytics": "クラッシュ分析",
|
"crashlytics": "クラッシュ分析",
|
||||||
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
|
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
|
||||||
"appendSystemDns": "システムDNSを追加",
|
"appendSystemDns": "システムDNSを追加",
|
||||||
"appendSystemDnsTip": "設定にシステム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": "遅延テスト"
|
||||||
}
|
}
|
||||||
@@ -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": "Переименовать",
|
||||||
@@ -435,5 +437,53 @@
|
|||||||
"crashlytics": "Анализ сбоев",
|
"crashlytics": "Анализ сбоев",
|
||||||
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
|
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
|
||||||
"appendSystemDns": "Добавить системный DNS",
|
"appendSystemDns": "Добавить системный DNS",
|
||||||
"appendSystemDnsTip": "Принудительно добавить системный 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": "Тест задержки"
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
@@ -435,5 +429,53 @@
|
|||||||
"crashlytics": "崩溃分析",
|
"crashlytics": "崩溃分析",
|
||||||
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
|
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
|
||||||
"appendSystemDns": "追加系统DNS",
|
"appendSystemDns": "追加系统DNS",
|
||||||
"appendSystemDnsTip": "强制为配置附加系统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": "延迟测试"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
23
assets/images/empty/connection.svg
Normal 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 |
9
assets/images/empty/data.svg
Normal 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 |
25
assets/images/empty/log.svg
Normal 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 |
37
assets/images/empty/profile.svg
Normal 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 |
18
assets/images/empty/proxy.svg
Normal 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 |
31
assets/images/empty/rule.svg
Normal 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 |
19
assets/images/empty/script.svg
Normal 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 |
BIN
assets/images/icon/status_1.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/images/icon/status_1.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/images/icon/status_2.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/images/icon/status_2.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/images/icon/status_3.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/images/icon/status_3.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -36,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() {
|
||||||
@@ -240,37 +235,19 @@ 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())
|
||||||
}
|
}
|
||||||
hub.ApplyConfig(currentConfig)
|
hub.ApplyConfig(currentConfig)
|
||||||
patchSelectGroup(params.SelectedMap)
|
patchSelectGroup(params.SelectedMap)
|
||||||
updateListeners()
|
updateListeners()
|
||||||
runtime.GC()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
47
core/go.mod
@@ -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,26 +19,22 @@ 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.9.0 // indirect
|
github.com/enfein/mieru/v3 v3.26.2 // indirect
|
||||||
github.com/enfein/mieru/v3 v3.20.0 // 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/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/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
|
||||||
@@ -46,45 +43,50 @@ require (
|
|||||||
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-20250902133113-a7f637c14281 // 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.24.0 // 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-20250919004547-6122b699a301 // indirect
|
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
|
||||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b // 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.6 // indirect
|
github.com/metacubex/sing v0.5.6 // indirect
|
||||||
github.com/metacubex/sing-mux v0.3.4 // indirect
|
github.com/metacubex/sing-mux v0.3.4 // indirect
|
||||||
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 // 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.7 // 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.8 // indirect
|
github.com/metacubex/sing-tun v0.4.11 // indirect
|
||||||
github.com/metacubex/sing-vmess v0.2.4 // 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-20250922175018-15c9a6a78719 // indirect
|
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect
|
||||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect
|
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // indirect
|
||||||
github.com/metacubex/utls v1.8.1 // 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/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/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/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
|
||||||
@@ -96,7 +98,6 @@ require (
|
|||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // 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
|
||||||
@@ -104,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
|
||||||
|
|||||||
110
core/go.sum
@@ -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.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
github.com/enfein/mieru/v3 v3.26.2 h1:U/2XJc+3vrJD9r815FoFdwToQFEcqSOzzzWIPPhjfEU=
|
||||||
github.com/ebitengine/purego v0.9.0/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.20.0 h1:1ob7pCIVSH5FYFAfYvim8isLW1vBOS4cFOUF9exJS38=
|
|
||||||
github.com/enfein/mieru/v3 v3.20.0/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,35 +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.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 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
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.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/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=
|
||||||
@@ -86,59 +74,74 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
|||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/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-20250902133113-a7f637c14281 h1:09EM0sOLb2kfL0KETGhHujsBLB5iy5U/2yHRHsxf/pI=
|
github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c=
|
||||||
github.com/metacubex/amneziawg-go v0.0.0-20250902133113-a7f637c14281/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.24.0 h1:EyNiPeVOlg0joSHTzi5oentI0j5M89utUq/5dd76pWM=
|
github.com/metacubex/bart v0.26.0 h1:d/bBTvVatfVWGfQbiDpYKI1bXUJgjaabB2KpK1Tnk6w=
|
||||||
github.com/metacubex/bart v0.24.0/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-20250919004547-6122b699a301 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ=
|
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ=
|
||||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b h1:z7JLKjugnQ1qvDOAD8yMA5C8AlJY3bG+VrrgRntRlUY=
|
github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
|
||||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs=
|
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.6 h1:mEPDCadsCj3DB8gn+t/EtposlYuALEkExa/LUguw6/c=
|
||||||
github.com/metacubex/sing v0.5.6/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
github.com/metacubex/sing v0.5.6/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||||
github.com/metacubex/sing-mux v0.3.4 h1:tf4r27CIkzaxq9kBlAXQkgMXq2HPp5Mta60Kb4RCZF0=
|
github.com/metacubex/sing-mux v0.3.4 h1:tf4r27CIkzaxq9kBlAXQkgMXq2HPp5Mta60Kb4RCZF0=
|
||||||
github.com/metacubex/sing-mux v0.3.4/go.mod h1:SEJfAuykNj/ozbPqngEYqyggwSr81+L7Nu09NRD5mh4=
|
github.com/metacubex/sing-mux v0.3.4/go.mod h1:SEJfAuykNj/ozbPqngEYqyggwSr81+L7Nu09NRD5mh4=
|
||||||
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg=
|
github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e h1:MLxp42z9Jd6LtY2suyawnl24oNzIsFxWc15bNeDIGxA=
|
||||||
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
|
github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e/go.mod h1:+lgKTd52xAarGtqugALISShyw4KxnoEpYe2u0zJh26w=
|
||||||
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.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
|
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.2.7/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.8 h1:3PyiUKWXYi37yHptXskzL1723O3OUdyt0Aej4XHVikM=
|
github.com/metacubex/sing-tun v0.4.11 h1:NG5zpvYPbBXf+9GSUmDaGCDwl3hZXV677tbRAw0QtCM=
|
||||||
github.com/metacubex/sing-tun v0.4.8/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
github.com/metacubex/sing-tun v0.4.11/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
||||||
github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkAaus6I=
|
github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkAaus6I=
|
||||||
github.com/metacubex/sing-vmess v0.2.4/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-20250922175018-15c9a6a78719 h1:T6qCCfolRDAVJKeaPW/mXwNLjnlo65AYN7WS2jrBNaM=
|
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk=
|
||||||
github.com/metacubex/smux v0.0.0-20250922175018-15c9a6a78719/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-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
|
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
|
||||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/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 h1:RW8GeCGWAegjV0HW5nw9DoqNoeGAXXeYUP6AysmRvx4=
|
github.com/metacubex/tls v0.1.1 h1:BEcZrsPTTfNf4sKZ02EbZodv4UIj7fgHWa1Eqo12Bc0=
|
||||||
github.com/metacubex/utls v1.8.1/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
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 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E=
|
||||||
@@ -149,9 +152,6 @@ github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd
|
|||||||
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=
|
||||||
@@ -164,14 +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/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/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,16 +177,9 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
|
|||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
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=
|
||||||
@@ -211,7 +200,6 @@ gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+r
|
|||||||
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=
|
||||||
@@ -235,22 +223,20 @@ 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-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-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.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.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=
|
||||||
|
|||||||
70
core/hub.go
@@ -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"
|
||||||
@@ -19,11 +20,11 @@ import (
|
|||||||
"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"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -43,10 +44,8 @@ func handleInitClash(paramsString string) bool {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +96,52 @@ func handleValidateConfig(path string) string {
|
|||||||
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)) {
|
||||||
@@ -143,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,
|
||||||
@@ -157,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,
|
||||||
@@ -287,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 ""
|
||||||
@@ -489,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()
|
||||||
}
|
}
|
||||||
|
|||||||
23
core/lib.go
@@ -201,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 {
|
||||||
@@ -241,6 +261,9 @@ func sendMessage(message Message) {
|
|||||||
//export stopTun
|
//export stopTun
|
||||||
func stopTun() {
|
func stopTun() {
|
||||||
handleStopTun()
|
handleStopTun()
|
||||||
|
if isRunning {
|
||||||
|
handleStopListener()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//export suspend
|
//export suspend
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,21 @@ 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 'hive.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 +36,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';
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,17 @@ 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 watchExecution = true;
|
||||||
|
|
||||||
final defaultTextScaleFactor =
|
final defaultTextScaleFactor =
|
||||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||||
@@ -57,18 +65,26 @@ final commonFilter = ImageFilter.blur(
|
|||||||
tileMode: TileMode.mirror,
|
tileMode: TileMode.mirror,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 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 +102,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 +126,6 @@ const scriptTemplate = '''
|
|||||||
const main = (config) => {
|
const main = (config) => {
|
||||||
return config;
|
return config;
|
||||||
}''';
|
}''';
|
||||||
|
|
||||||
|
const backupDatabaseName = 'database.sqlite';
|
||||||
|
const configJsonName = 'config.json';
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:fl_clash/manager/message_manager.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/scaffold.dart';
|
import 'package:fl_clash/widgets/scaffold.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@@ -7,8 +9,11 @@ extension BuildContextExtension on BuildContext {
|
|||||||
return findAncestorStateOfType<CommonScaffoldState>();
|
return findAncestorStateOfType<CommonScaffoldState>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void showNotifier(String text) {
|
void showNotifier(String text, {MessageActionState? actionState}) {
|
||||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
return findAncestorStateOfType<StatusManagerState>()?.message(
|
||||||
|
text,
|
||||||
|
actionState: actionState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||||
@@ -42,6 +47,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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
260
lib/common/indexing.dart
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final indexing = Indexing();
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,26 +8,16 @@ 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(
|
Size computeTextSize(Text text, {double maxWidth = double.infinity}) {
|
||||||
Text text, {
|
|
||||||
double maxWidth = double.infinity,
|
|
||||||
}) {
|
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(text: text.data, style: text.style),
|
||||||
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);
|
||||||
maxWidth: maxWidth,
|
|
||||||
);
|
|
||||||
return textPainter.size;
|
return textPainter.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +25,7 @@ class Measure {
|
|||||||
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 +33,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 +50,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 +68,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 +77,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 +86,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
@@ -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();
|
||||||
@@ -2,17 +2,21 @@ import 'package:riverpod/riverpod.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.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 +24,20 @@ 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) {
|
|
||||||
final res = super.updateShouldNotify(previous, next);
|
|
||||||
if (res) {
|
|
||||||
onUpdate(next);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onUpdate(T value) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -61,19 +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 validateFilePath async {
|
Future<String> get sharedFilePath async {
|
||||||
final homeDirPath = await appPath.homeDirPath;
|
final mHomeDirPath = await homeDirPath;
|
||||||
return join(homeDirPath, 'temp', 'validate${utils.id}.yaml');
|
return join(mHomeDirPath, 'shared.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> get sharedPreferencesPath async {
|
Future<String> get sharedPreferencesPath async {
|
||||||
@@ -86,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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
33
lib/common/store.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,8 +260,9 @@ class Windows {
|
|||||||
await Future.delayed(Duration(milliseconds: 300));
|
await Future.delayed(Duration(milliseconds: 300));
|
||||||
final retryStatus = await retry(
|
final retryStatus = await retry(
|
||||||
task: checkService,
|
task: checkService,
|
||||||
retryIf: (status) => status == WindowsHelperServiceStatus.running,
|
maxAttempts: 5,
|
||||||
delay: commonDuration,
|
retryIf: (status) => status != WindowsHelperServiceStatus.running,
|
||||||
|
delay: Duration(seconds: 1),
|
||||||
);
|
);
|
||||||
return res && retryStatus == WindowsHelperServiceStatus.running;
|
return res && retryStatus == WindowsHelperServiceStatus.running;
|
||||||
}
|
}
|
||||||
|
|||||||
612
lib/common/task.dart
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/database/database.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
Future<T> decodeJSONTask<T>(String data) async {
|
||||||
|
return await compute<String, T>(_decodeJSON, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _decodeJSON<T>(String content) async {
|
||||||
|
return json.decode(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> encodeJSONTask<T>(T data) async {
|
||||||
|
return await compute<T, String>(_encodeJSON, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _encodeJSON<T>(T content) async {
|
||||||
|
return json.encode(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> encodeYamlTask<T>(T data) async {
|
||||||
|
return await compute<T, String>(_encodeYaml, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _encodeYaml<T>(T content) async {
|
||||||
|
return yaml.encode(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Group>> toGroupsTask(ComputeGroupsState data) async {
|
||||||
|
return await compute<ComputeGroupsState, List<Group>>(_toGroupsTask, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Group>> _toGroupsTask(ComputeGroupsState state) async {
|
||||||
|
final proxiesData = state.proxiesData;
|
||||||
|
final all = proxiesData.all;
|
||||||
|
final sortType = state.sortType;
|
||||||
|
final delayMap = state.delayMap;
|
||||||
|
final selectedMap = state.selectedMap;
|
||||||
|
final defaultTestUrl = state.defaultTestUrl;
|
||||||
|
final proxies = proxiesData.proxies;
|
||||||
|
if (proxies.isEmpty) return [];
|
||||||
|
final groupsRaw = all
|
||||||
|
.where((name) {
|
||||||
|
final proxy = proxies[name] ?? {};
|
||||||
|
return GroupTypeExtension.valueList.contains(proxy['type']);
|
||||||
|
})
|
||||||
|
.map((groupName) {
|
||||||
|
final group = proxies[groupName];
|
||||||
|
group['all'] = ((group['all'] ?? []) as List)
|
||||||
|
.map((name) => proxies[name])
|
||||||
|
.where((proxy) => proxy != null)
|
||||||
|
.toList();
|
||||||
|
return group;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
final groups = groupsRaw.map((e) => Group.fromJson(e)).toList();
|
||||||
|
return computeSort(
|
||||||
|
groups: groups,
|
||||||
|
sortType: sortType,
|
||||||
|
delayMap: delayMap,
|
||||||
|
selectedMap: selectedMap,
|
||||||
|
defaultTestUrl: defaultTestUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> makeRealProfileTask(
|
||||||
|
MakeRealProfileState data,
|
||||||
|
) async {
|
||||||
|
return await compute<MakeRealProfileState, Map<String, dynamic>>(
|
||||||
|
_makeRealProfileTask,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> _makeRealProfileTask(
|
||||||
|
MakeRealProfileState data,
|
||||||
|
) async {
|
||||||
|
final rawConfig = Map.from(data.rawConfig);
|
||||||
|
final realPatchConfig = data.realPatchConfig;
|
||||||
|
final profilesPath = data.profilesPath;
|
||||||
|
final profileId = data.profileId;
|
||||||
|
final overrideDns = data.overrideDns;
|
||||||
|
final addedRules = data.addedRules;
|
||||||
|
final appendSystemDns = data.appendSystemDns;
|
||||||
|
final defaultUA = data.defaultUA;
|
||||||
|
String getProvidersFilePathInner(String type, String url) {
|
||||||
|
return join(
|
||||||
|
profilesPath,
|
||||||
|
'providers',
|
||||||
|
profileId.toString(),
|
||||||
|
type,
|
||||||
|
url.toMd5(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConfig['external-controller'] = realPatchConfig.externalController.value;
|
||||||
|
rawConfig['external-ui'] = '';
|
||||||
|
rawConfig['interface-name'] = '';
|
||||||
|
rawConfig['external-ui-url'] = '';
|
||||||
|
rawConfig['tcp-concurrent'] = realPatchConfig.tcpConcurrent;
|
||||||
|
rawConfig['unified-delay'] = realPatchConfig.unifiedDelay;
|
||||||
|
rawConfig['ipv6'] = realPatchConfig.ipv6;
|
||||||
|
rawConfig['log-level'] = realPatchConfig.logLevel.name;
|
||||||
|
rawConfig['port'] = 0;
|
||||||
|
rawConfig['socks-port'] = 0;
|
||||||
|
rawConfig['keep-alive-interval'] = realPatchConfig.keepAliveInterval;
|
||||||
|
rawConfig['mixed-port'] = realPatchConfig.mixedPort;
|
||||||
|
rawConfig['port'] = realPatchConfig.port;
|
||||||
|
rawConfig['socks-port'] = realPatchConfig.socksPort;
|
||||||
|
rawConfig['redir-port'] = realPatchConfig.redirPort;
|
||||||
|
rawConfig['tproxy-port'] = realPatchConfig.tproxyPort;
|
||||||
|
rawConfig['find-process-mode'] = realPatchConfig.findProcessMode.name;
|
||||||
|
rawConfig['allow-lan'] = realPatchConfig.allowLan;
|
||||||
|
rawConfig['mode'] = realPatchConfig.mode.name;
|
||||||
|
if (rawConfig['tun'] == null) {
|
||||||
|
rawConfig['tun'] = {};
|
||||||
|
}
|
||||||
|
rawConfig['tun']['enable'] = realPatchConfig.tun.enable;
|
||||||
|
rawConfig['tun']['device'] = realPatchConfig.tun.device;
|
||||||
|
rawConfig['tun']['dns-hijack'] = realPatchConfig.tun.dnsHijack;
|
||||||
|
rawConfig['tun']['stack'] = realPatchConfig.tun.stack.name;
|
||||||
|
rawConfig['tun']['route-address'] = realPatchConfig.tun.routeAddress;
|
||||||
|
rawConfig['tun']['auto-route'] = realPatchConfig.tun.autoRoute;
|
||||||
|
rawConfig['geodata-loader'] = realPatchConfig.geodataLoader.name;
|
||||||
|
if (rawConfig['sniffer']?['sniff'] != null) {
|
||||||
|
for (final value in (rawConfig['sniffer']?['sniff'] as Map).values) {
|
||||||
|
if (value['ports'] != null && value['ports'] is List) {
|
||||||
|
value['ports'] =
|
||||||
|
value['ports']?.map((item) => item.toString()).toList() ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawConfig['profile'] == null) {
|
||||||
|
rawConfig['profile'] = {};
|
||||||
|
}
|
||||||
|
if (rawConfig['proxy-providers'] != null) {
|
||||||
|
final proxyProviders = rawConfig['proxy-providers'] as Map;
|
||||||
|
for (final key in proxyProviders.keys) {
|
||||||
|
final proxyProvider = proxyProviders[key];
|
||||||
|
if (proxyProvider['type'] != 'http') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (proxyProvider['url'] != null) {
|
||||||
|
proxyProvider['path'] = getProvidersFilePathInner(
|
||||||
|
'proxies',
|
||||||
|
proxyProvider['url'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawConfig['rule-providers'] != null) {
|
||||||
|
final ruleProviders = rawConfig['rule-providers'] as Map;
|
||||||
|
for (final key in ruleProviders.keys) {
|
||||||
|
final ruleProvider = ruleProviders[key];
|
||||||
|
if (ruleProvider['type'] != 'http') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ruleProvider['url'] != null) {
|
||||||
|
ruleProvider['path'] = getProvidersFilePathInner(
|
||||||
|
'rules',
|
||||||
|
ruleProvider['url'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rawConfig['profile']['store-selected'] = false;
|
||||||
|
rawConfig['geox-url'] = realPatchConfig.geoXUrl.toJson();
|
||||||
|
rawConfig['global-ua'] = realPatchConfig.globalUa ?? defaultUA;
|
||||||
|
if (rawConfig['hosts'] == null) {
|
||||||
|
rawConfig['hosts'] = {};
|
||||||
|
}
|
||||||
|
for (final host in realPatchConfig.hosts.entries) {
|
||||||
|
rawConfig['hosts'][host.key] = host.value.splitByMultipleSeparators;
|
||||||
|
}
|
||||||
|
if (rawConfig['dns'] == null) {
|
||||||
|
rawConfig['dns'] = {};
|
||||||
|
}
|
||||||
|
final isEnableDns = rawConfig['dns']['enable'] == true;
|
||||||
|
final systemDns = 'system://';
|
||||||
|
if (overrideDns || !isEnableDns) {
|
||||||
|
final dns = switch (!isEnableDns) {
|
||||||
|
true => realPatchConfig.dns.copyWith(
|
||||||
|
nameserver: [...realPatchConfig.dns.nameserver, systemDns],
|
||||||
|
),
|
||||||
|
false => realPatchConfig.dns,
|
||||||
|
};
|
||||||
|
rawConfig['dns'] = dns.toJson();
|
||||||
|
rawConfig['dns']['nameserver-policy'] = {};
|
||||||
|
for (final entry in dns.nameserverPolicy.entries) {
|
||||||
|
rawConfig['dns']['nameserver-policy'][entry.key] =
|
||||||
|
entry.value.splitByMultipleSeparators;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (appendSystemDns) {
|
||||||
|
final List<String> nameserver = List<String>.from(
|
||||||
|
rawConfig['dns']['nameserver'] ?? [],
|
||||||
|
);
|
||||||
|
if (!nameserver.contains(systemDns)) {
|
||||||
|
rawConfig['dns']['nameserver'] = [...nameserver, systemDns];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> rules = [];
|
||||||
|
if (rawConfig['rules'] != null) {
|
||||||
|
rules = List<String>.from(rawConfig['rules']);
|
||||||
|
}
|
||||||
|
rawConfig.remove('rules');
|
||||||
|
if (addedRules.isNotEmpty) {
|
||||||
|
final parsedNewRules = addedRules
|
||||||
|
.map((item) => ParsedRule.parseString(item.value))
|
||||||
|
.toList();
|
||||||
|
final hasMatchPlaceholder = parsedNewRules.any(
|
||||||
|
(item) => item.ruleTarget?.toUpperCase() == 'MATCH',
|
||||||
|
);
|
||||||
|
String? replacementTarget;
|
||||||
|
|
||||||
|
if (hasMatchPlaceholder) {
|
||||||
|
for (int i = rules.length - 1; i >= 0; i--) {
|
||||||
|
final parsed = ParsedRule.parseString(rules[i]);
|
||||||
|
if (parsed.ruleAction == RuleAction.MATCH) {
|
||||||
|
final target = parsed.ruleTarget;
|
||||||
|
if (target != null && target.isNotEmpty) {
|
||||||
|
replacementTarget = target;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final List<String> finalAddedRules;
|
||||||
|
|
||||||
|
if (replacementTarget?.isNotEmpty == true) {
|
||||||
|
finalAddedRules = [];
|
||||||
|
for (int i = 0; i < parsedNewRules.length; i++) {
|
||||||
|
final parsed = parsedNewRules[i];
|
||||||
|
if (parsed.ruleTarget?.toUpperCase() == 'MATCH') {
|
||||||
|
finalAddedRules.add(
|
||||||
|
parsed.copyWith(ruleTarget: replacementTarget).value,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalAddedRules.add(addedRules[i].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalAddedRules = addedRules.map((e) => e.value).toList();
|
||||||
|
}
|
||||||
|
rules = [...finalAddedRules, ...rules];
|
||||||
|
}
|
||||||
|
rawConfig['rules'] = rules;
|
||||||
|
return Map<String, dynamic>.from(rawConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> shakingProfileTask(
|
||||||
|
VM2<Iterable<int>, Iterable<int>> data,
|
||||||
|
) async {
|
||||||
|
return await compute<
|
||||||
|
VM3<Iterable<int>, Iterable<int>, RootIsolateToken>,
|
||||||
|
List<String>
|
||||||
|
>(_shakingProfileTask, VM3(data.a, data.b, RootIsolateToken.instance!));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _shakingProfileTask(
|
||||||
|
VM3<Iterable<int>, Iterable<int>, RootIsolateToken> data,
|
||||||
|
) async {
|
||||||
|
final profileIds = data.a;
|
||||||
|
final scriptIds = data.b;
|
||||||
|
final token = data.c;
|
||||||
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
|
final profilesDir = Directory(await appPath.profilesPath);
|
||||||
|
final scriptsDir = Directory(await appPath.scriptsDirPath);
|
||||||
|
final providersDir = Directory(await appPath.getProvidersRootPath());
|
||||||
|
final List<String> targets = [];
|
||||||
|
void scanDirectory(
|
||||||
|
Directory dir,
|
||||||
|
Iterable<int> baseNames, {
|
||||||
|
bool skipProvidersFolder = false,
|
||||||
|
}) {
|
||||||
|
if (!dir.existsSync()) return;
|
||||||
|
final entities = dir.listSync(recursive: false, followLinks: false);
|
||||||
|
|
||||||
|
for (final entity in entities) {
|
||||||
|
if (entity is File) {
|
||||||
|
final id = basenameWithoutExtension(entity.path);
|
||||||
|
if (!baseNames.contains(int.tryParse(id))) {
|
||||||
|
targets.add(entity.path);
|
||||||
|
}
|
||||||
|
} else if (skipProvidersFolder && entity is Directory) {
|
||||||
|
if (basename(entity.path) == 'providers') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanDirectory(profilesDir, profileIds, skipProvidersFolder: true);
|
||||||
|
scanDirectory(providersDir, profileIds);
|
||||||
|
scanDirectory(scriptsDir, scriptIds);
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> encodeLogsTask(List<Log> data) async {
|
||||||
|
return await compute<List<Log>, String>(_encodeLogsTask, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _encodeLogsTask(List<Log> data) async {
|
||||||
|
final logsRaw = data.map((item) => item.toString());
|
||||||
|
final logsRawString = logsRaw.join('\n');
|
||||||
|
return logsRawString;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MigrationData> oldToNowTask(Map<String, Object?> data) async {
|
||||||
|
final homeDir = await appPath.homeDirPath;
|
||||||
|
return await compute<
|
||||||
|
VM3<Map<String, Object?>, String, String>,
|
||||||
|
MigrationData
|
||||||
|
>(_oldToNowTask, VM3(data, homeDir, homeDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MigrationData> _oldToNowTask(
|
||||||
|
VM3<Map<String, Object?>, String, String> data,
|
||||||
|
) async {
|
||||||
|
final configMap = data.a;
|
||||||
|
final sourcePath = data.b;
|
||||||
|
final targetPath = data.c;
|
||||||
|
|
||||||
|
final accessControlMap = configMap['accessControl'];
|
||||||
|
final isAccessControl = configMap['isAccessControl'];
|
||||||
|
if (accessControlMap != null) {
|
||||||
|
(accessControlMap as Map)['enable'] = isAccessControl;
|
||||||
|
if (configMap['vpnProps'] != null) {
|
||||||
|
final vpnPropsRaw = configMap['vpnProps'] as Map;
|
||||||
|
vpnPropsRaw['accessControl'] = accessControlMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (configMap['vpnProps'] != null) {
|
||||||
|
final vpnPropsRaw = configMap['vpnProps'] as Map;
|
||||||
|
vpnPropsRaw['accessControlProps'] = vpnPropsRaw['accessControl'];
|
||||||
|
}
|
||||||
|
configMap['davProps'] = configMap['dav'];
|
||||||
|
final appSettingProps = configMap['appSetting'] as Map? ?? {};
|
||||||
|
appSettingProps['restoreStrategy'] = appSettingProps['recoveryStrategy'];
|
||||||
|
configMap['appSettingProps'] = appSettingProps;
|
||||||
|
configMap['proxiesStyleProps'] = configMap['proxiesStyle'];
|
||||||
|
configMap['proxiesStyleProps'] = configMap['proxiesStyle'];
|
||||||
|
// final overwriteMap = configMap['overwrite'] as Map? ?? {};
|
||||||
|
// configMap['overwriteType'] = overwriteMap['type'];
|
||||||
|
// configMap['scriptId'] = overwriteMap['scriptOverwrite'];
|
||||||
|
List rawScripts = configMap['scripts'] as List<dynamic>? ?? [];
|
||||||
|
if (rawScripts.isEmpty) {
|
||||||
|
final scriptPropsJson = configMap['scriptProps'] as Map<String, dynamic>?;
|
||||||
|
if (scriptPropsJson != null) {
|
||||||
|
rawScripts = scriptPropsJson['scripts'] as List<dynamic>? ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Map<String, int> idMap = {};
|
||||||
|
final List<Script> scripts = [];
|
||||||
|
for (final rawScript in rawScripts) {
|
||||||
|
final id = rawScript['id'] as String?;
|
||||||
|
final content = rawScript['content'] as String?;
|
||||||
|
final label = rawScript['label'] as String?;
|
||||||
|
if (id == null || content == null || label == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final newId = idMap.updateCacheValue(rawScript['id'], () => snowflake.id);
|
||||||
|
final path = _getScriptPath(targetPath, newId.toString());
|
||||||
|
final file = File(path);
|
||||||
|
await file.safeWriteAsString(content);
|
||||||
|
scripts.add(
|
||||||
|
Script(id: newId, label: label, lastUpdateTime: DateTime.now()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List rawRules = configMap['rules'] as List<dynamic>? ?? [];
|
||||||
|
final List<Rule> rules = [];
|
||||||
|
final List<ProfileRuleLink> links = [];
|
||||||
|
for (final rawRule in rawRules) {
|
||||||
|
final id = idMap.updateCacheValue(rawRule['id'], () => snowflake.id);
|
||||||
|
rawRule['id'] = id;
|
||||||
|
rules.add(Rule.fromJson(rawRule));
|
||||||
|
links.add(ProfileRuleLink(ruleId: id));
|
||||||
|
}
|
||||||
|
List rawProfiles = configMap['profiles'] as List<dynamic>? ?? [];
|
||||||
|
final List<Profile> profiles = [];
|
||||||
|
for (final rawProfile in rawProfiles) {
|
||||||
|
final rawId = rawProfile['id'] as String?;
|
||||||
|
if (rawId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final profileId = idMap.updateCacheValue(rawId, () => snowflake.id);
|
||||||
|
rawProfile['id'] = profileId;
|
||||||
|
final overwrite = rawProfile['overwrite'] as Map?;
|
||||||
|
if (overwrite != null) {
|
||||||
|
final standardOverwrite = overwrite['standardOverwrite'] as Map?;
|
||||||
|
if (standardOverwrite != null) {
|
||||||
|
final addedRules = standardOverwrite['addedRules'] as List? ?? [];
|
||||||
|
for (final addRule in addedRules) {
|
||||||
|
final id = idMap.updateCacheValue(addRule['id'], () => snowflake.id);
|
||||||
|
addRule['id'] = id;
|
||||||
|
rules.add(Rule.fromJson(addRule));
|
||||||
|
links.add(
|
||||||
|
ProfileRuleLink(
|
||||||
|
profileId: profileId,
|
||||||
|
ruleId: id,
|
||||||
|
scene: RuleScene.added,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final disabledRuleIds = standardOverwrite['disabledRuleIds'] as List?;
|
||||||
|
if (disabledRuleIds != null) {
|
||||||
|
for (final disabledRuleId in disabledRuleIds) {
|
||||||
|
final newDisabledRuleId = idMap[disabledRuleId];
|
||||||
|
if (newDisabledRuleId != null) {
|
||||||
|
links.add(
|
||||||
|
ProfileRuleLink(
|
||||||
|
profileId: profileId,
|
||||||
|
ruleId: newDisabledRuleId,
|
||||||
|
scene: RuleScene.disabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final scriptOverwrite = overwrite['scriptOverwrite'] as Map?;
|
||||||
|
if (scriptOverwrite != null) {
|
||||||
|
final scriptId = scriptOverwrite['scriptId'] as String?;
|
||||||
|
rawProfile['scriptId'] = scriptId != null ? idMap[scriptId] : null;
|
||||||
|
}
|
||||||
|
rawProfile['overwriteType'] = overwrite['type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
final sourceFile = File(_getProfilePath(sourcePath, rawId));
|
||||||
|
final targetFilePath = _getProfilePath(targetPath, profileId.toString());
|
||||||
|
await sourceFile.safeCopy(targetFilePath);
|
||||||
|
profiles.add(Profile.fromJson(rawProfile));
|
||||||
|
}
|
||||||
|
final currentProfileId = configMap['currentProfileId'];
|
||||||
|
configMap['currentProfileId'] = currentProfileId != null
|
||||||
|
? idMap[currentProfileId]
|
||||||
|
: null;
|
||||||
|
return MigrationData(
|
||||||
|
configMap: configMap,
|
||||||
|
profiles: profiles,
|
||||||
|
rules: rules,
|
||||||
|
scripts: scripts,
|
||||||
|
links: links,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> backupTask(
|
||||||
|
Map<String, dynamic> configMap,
|
||||||
|
Iterable<String> fileNames,
|
||||||
|
) async {
|
||||||
|
return await compute<
|
||||||
|
VM3<Map<String, dynamic>, Iterable<String>, RootIsolateToken>,
|
||||||
|
String
|
||||||
|
>(_backupTask, VM3(configMap, fileNames, RootIsolateToken.instance!));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _backupTask<T>(
|
||||||
|
VM3<Map<String, dynamic>, Iterable<String>, RootIsolateToken> args,
|
||||||
|
) async {
|
||||||
|
final configMap = args.a;
|
||||||
|
final fileNames = args.b;
|
||||||
|
final token = args.c;
|
||||||
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
|
final dbPath = await appPath.databasePath;
|
||||||
|
final configStr = json.encode(configMap);
|
||||||
|
final profilesDir = Directory(await appPath.profilesPath);
|
||||||
|
final scriptsDir = Directory(await appPath.scriptsDirPath);
|
||||||
|
final tempZipFilePath = await appPath.tempFilePath;
|
||||||
|
final tempDBFile = File(await appPath.tempFilePath);
|
||||||
|
final tempConfigFile = File(await appPath.tempFilePath);
|
||||||
|
final dbFile = File(dbPath);
|
||||||
|
if (await dbFile.exists()) {
|
||||||
|
await dbFile.copy(tempDBFile.path);
|
||||||
|
}
|
||||||
|
final encoder = ZipFileEncoder();
|
||||||
|
encoder.create(tempZipFilePath);
|
||||||
|
await tempConfigFile.writeAsString(configStr);
|
||||||
|
await encoder.addFile(tempDBFile, backupDatabaseName);
|
||||||
|
await encoder.addFile(tempConfigFile, configJsonName);
|
||||||
|
if (await profilesDir.exists()) {
|
||||||
|
await encoder.addDirectory(
|
||||||
|
profilesDir,
|
||||||
|
filter: (file, _) {
|
||||||
|
if (!fileNames.contains(basename(file.path))) {
|
||||||
|
return ZipFileOperation.skip;
|
||||||
|
}
|
||||||
|
return ZipFileOperation.include;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (await scriptsDir.exists()) {
|
||||||
|
await encoder.addDirectory(
|
||||||
|
scriptsDir,
|
||||||
|
filter: (file, _) {
|
||||||
|
if (!fileNames.contains(basename(file.path))) {
|
||||||
|
return ZipFileOperation.skip;
|
||||||
|
}
|
||||||
|
return ZipFileOperation.include;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
encoder.close();
|
||||||
|
await tempConfigFile.safeDelete();
|
||||||
|
await tempDBFile.safeDelete();
|
||||||
|
return tempZipFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MigrationData> restoreTask() async {
|
||||||
|
return await compute<RootIsolateToken, MigrationData>(
|
||||||
|
_restoreTask,
|
||||||
|
RootIsolateToken.instance!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MigrationData> _restoreTask(RootIsolateToken token) async {
|
||||||
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
|
final backupFilePath = await appPath.backupFilePath;
|
||||||
|
final restoreDirPath = await appPath.restoreDirPath;
|
||||||
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
|
final zipDecoder = ZipDecoder();
|
||||||
|
final input = InputFileStream(backupFilePath);
|
||||||
|
final archive = zipDecoder.decodeStream(input);
|
||||||
|
final dir = Directory(restoreDirPath);
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
for (final file in archive.files) {
|
||||||
|
final outPath = join(restoreDirPath, posix.normalize(file.name));
|
||||||
|
final outputStream = OutputFileStream(outPath);
|
||||||
|
file.writeContent(outputStream);
|
||||||
|
await outputStream.close();
|
||||||
|
}
|
||||||
|
await input.close();
|
||||||
|
final restoreConfigFile = File(join(restoreDirPath, configJsonName));
|
||||||
|
if (!await restoreConfigFile.exists()) {
|
||||||
|
throw appLocalizations.invalidBackupFile;
|
||||||
|
}
|
||||||
|
final restoreConfigMap =
|
||||||
|
json.decode(await restoreConfigFile.readAsString())
|
||||||
|
as Map<String, Object?>?;
|
||||||
|
final version = restoreConfigMap?['version'] ?? 0;
|
||||||
|
MigrationData migrationData = MigrationData(configMap: restoreConfigMap);
|
||||||
|
if (version == 0 && restoreConfigMap != null) {
|
||||||
|
migrationData = await _oldToNowTask(
|
||||||
|
VM3(restoreConfigMap, restoreDirPath, homeDirPath),
|
||||||
|
);
|
||||||
|
return migrationData;
|
||||||
|
}
|
||||||
|
final backupDatabaseFile = File(join(restoreDirPath, backupDatabaseName));
|
||||||
|
if (!await backupDatabaseFile.exists()) {
|
||||||
|
return migrationData;
|
||||||
|
}
|
||||||
|
final database = Database(
|
||||||
|
driftDatabase(
|
||||||
|
name: 'database',
|
||||||
|
native: DriftNativeOptions(
|
||||||
|
databaseDirectory: () async => Directory(restoreDirPath),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final results = await Future.wait([
|
||||||
|
database.profilesDao.all().get(),
|
||||||
|
database.scriptsDao.all().get(),
|
||||||
|
database.rules.all().map((item) => item.toRule()).get(),
|
||||||
|
database.profileRuleLinks.all().map((item) => item.toLink()).get(),
|
||||||
|
]);
|
||||||
|
final profiles = results[0].cast<Profile>();
|
||||||
|
final scripts = results[1].cast<Script>();
|
||||||
|
final profilesMigration = profiles.map(
|
||||||
|
(item) => VM2(
|
||||||
|
_getProfilePath(restoreDirPath, item.id.toString()),
|
||||||
|
_getProfilePath(homeDirPath, item.id.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final scriptsMigration = scripts.map(
|
||||||
|
(item) => VM2(
|
||||||
|
_getScriptPath(restoreDirPath, item.id.toString()),
|
||||||
|
_getScriptPath(homeDirPath, item.id.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await _copyWithMapList([...profilesMigration, ...scriptsMigration]);
|
||||||
|
migrationData = migrationData.copyWith(
|
||||||
|
profiles: profiles,
|
||||||
|
scripts: scripts,
|
||||||
|
rules: results[2].cast<Rule>(),
|
||||||
|
links: results[3].cast<ProfileRuleLink>(),
|
||||||
|
);
|
||||||
|
await database.close();
|
||||||
|
return migrationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyWithMapList(List<VM2<String, String>> copyMapList) async {
|
||||||
|
await Future.wait(
|
||||||
|
copyMapList.map((item) => File(item.a).safeCopy(item.b)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getScriptPath(String root, String fileName) {
|
||||||
|
return join(root, 'scripts', '$fileName.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getProfilePath(String root, String fileName) {
|
||||||
|
return join(root, 'profiles', '$fileName.yaml');
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_clash/common/utils.dart';
|
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/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
@@ -15,38 +13,60 @@ import 'system.dart';
|
|||||||
import 'window.dart';
|
import 'window.dart';
|
||||||
|
|
||||||
class Tray {
|
class Tray {
|
||||||
|
static Tray? _instance;
|
||||||
|
|
||||||
|
Tray._internal();
|
||||||
|
|
||||||
|
factory Tray() {
|
||||||
|
_instance ??= Tray._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get trayIconSuffix {
|
||||||
|
return system.isWindows ? 'ico' : 'png';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> destroy() async {
|
||||||
|
await trayManager.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getTryIcon({required bool isStart, required bool tunEnable}) {
|
||||||
|
if (system.isMacOS || !isStart) {
|
||||||
|
return 'assets/images/icon/status_1.$trayIconSuffix';
|
||||||
|
}
|
||||||
|
if (!tunEnable) {
|
||||||
|
return 'assets/images/icon/status_2.$trayIconSuffix';
|
||||||
|
}
|
||||||
|
return 'assets/images/icon/status_3.$trayIconSuffix';
|
||||||
|
}
|
||||||
|
|
||||||
Future _updateSystemTray({
|
Future _updateSystemTray({
|
||||||
required Brightness? brightness,
|
required bool isStart,
|
||||||
bool force = false,
|
required bool tunEnable,
|
||||||
}) async {
|
}) async {
|
||||||
if (Platform.isLinux || force) {
|
if (Platform.isLinux) {
|
||||||
await trayManager.destroy();
|
await trayManager.destroy();
|
||||||
}
|
}
|
||||||
await trayManager.setIcon(
|
await trayManager.setIcon(
|
||||||
utils.getTrayIconPath(
|
getTryIcon(isStart: isStart, tunEnable: tunEnable),
|
||||||
brightness: brightness ??
|
|
||||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
|
||||||
),
|
|
||||||
isTemplate: true,
|
isTemplate: true,
|
||||||
);
|
);
|
||||||
if (!Platform.isLinux) {
|
if (!Platform.isLinux) {
|
||||||
await trayManager.setToolTip(
|
await trayManager.setToolTip(appName);
|
||||||
appName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> update({
|
Future<void> update({
|
||||||
required TrayState trayState,
|
required TrayState trayState,
|
||||||
bool focus = false,
|
required Traffic traffic,
|
||||||
}) async {
|
}) async {
|
||||||
if (system.isAndroid) {
|
if (system.isAndroid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Platform.isLinux) {
|
if (!system.isLinux) {
|
||||||
await _updateSystemTray(
|
await _updateSystemTray(
|
||||||
brightness: trayState.brightness,
|
isStart: trayState.isStart,
|
||||||
force: focus,
|
tunEnable: trayState.tunEnable,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
List<MenuItem> menuItems = [];
|
List<MenuItem> menuItems = [];
|
||||||
@@ -60,18 +80,28 @@ class Tray {
|
|||||||
final startMenuItem = MenuItem.checkbox(
|
final startMenuItem = MenuItem.checkbox(
|
||||||
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||||
onClick: (_) async {
|
onClick: (_) async {
|
||||||
globalState.appController.updateStart();
|
appController.updateStart();
|
||||||
},
|
},
|
||||||
checked: false,
|
checked: false,
|
||||||
);
|
);
|
||||||
menuItems.add(startMenuItem);
|
menuItems.add(startMenuItem);
|
||||||
|
if (system.isMacOS) {
|
||||||
|
final speedStatistics = MenuItem.checkbox(
|
||||||
|
label: appLocalizations.speedStatistics,
|
||||||
|
onClick: (_) async {
|
||||||
|
appController.updateSpeedStatistics();
|
||||||
|
},
|
||||||
|
checked: trayState.showTrayTitle,
|
||||||
|
);
|
||||||
|
menuItems.add(speedStatistics);
|
||||||
|
}
|
||||||
menuItems.add(MenuItem.separator());
|
menuItems.add(MenuItem.separator());
|
||||||
for (final mode in Mode.values) {
|
for (final mode in Mode.values) {
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
label: Intl.message(mode.name),
|
label: Intl.message(mode.name),
|
||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
globalState.appController.changeMode(mode);
|
appController.changeMode(mode);
|
||||||
},
|
},
|
||||||
checked: mode == trayState.mode,
|
checked: mode == trayState.mode,
|
||||||
),
|
),
|
||||||
@@ -85,13 +115,10 @@ class Tray {
|
|||||||
subMenuItems.add(
|
subMenuItems.add(
|
||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
label: proxy.name,
|
label: proxy.name,
|
||||||
checked: trayState.selectedMap[group.name] == proxy.name,
|
checked:
|
||||||
|
appController.getSelectedProxyName(group.name) == proxy.name,
|
||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
final appController = globalState.appController;
|
appController.updateCurrentSelectedMap(group.name, proxy.name);
|
||||||
appController.updateCurrentSelectedMap(
|
|
||||||
group.name,
|
|
||||||
proxy.name,
|
|
||||||
);
|
|
||||||
appController.changeProxy(
|
appController.changeProxy(
|
||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
proxyName: proxy.name,
|
proxyName: proxy.name,
|
||||||
@@ -103,9 +130,7 @@ class Tray {
|
|||||||
menuItems.add(
|
menuItems.add(
|
||||||
MenuItem.submenu(
|
MenuItem.submenu(
|
||||||
label: group.name,
|
label: group.name,
|
||||||
submenu: Menu(
|
submenu: Menu(items: subMenuItems),
|
||||||
items: subMenuItems,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -118,7 +143,7 @@ class Tray {
|
|||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
label: appLocalizations.tun,
|
label: appLocalizations.tun,
|
||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
globalState.appController.updateTun();
|
appController.updateTun();
|
||||||
},
|
},
|
||||||
checked: trayState.tunEnable,
|
checked: trayState.tunEnable,
|
||||||
),
|
),
|
||||||
@@ -127,7 +152,7 @@ class Tray {
|
|||||||
MenuItem.checkbox(
|
MenuItem.checkbox(
|
||||||
label: appLocalizations.systemProxy,
|
label: appLocalizations.systemProxy,
|
||||||
onClick: (_) {
|
onClick: (_) {
|
||||||
globalState.appController.updateSystemProxy();
|
appController.updateSystemProxy();
|
||||||
},
|
},
|
||||||
checked: trayState.systemProxy,
|
checked: trayState.systemProxy,
|
||||||
),
|
),
|
||||||
@@ -137,7 +162,7 @@ class Tray {
|
|||||||
final autoStartMenuItem = MenuItem.checkbox(
|
final autoStartMenuItem = MenuItem.checkbox(
|
||||||
label: appLocalizations.autoLaunch,
|
label: appLocalizations.autoLaunch,
|
||||||
onClick: (_) async {
|
onClick: (_) async {
|
||||||
globalState.appController.updateAutoLaunch();
|
appController.updateAutoLaunch();
|
||||||
},
|
},
|
||||||
checked: trayState.autoLaunch,
|
checked: trayState.autoLaunch,
|
||||||
);
|
);
|
||||||
@@ -153,44 +178,43 @@ class Tray {
|
|||||||
final exitMenuItem = MenuItem(
|
final exitMenuItem = MenuItem(
|
||||||
label: appLocalizations.exit,
|
label: appLocalizations.exit,
|
||||||
onClick: (_) async {
|
onClick: (_) async {
|
||||||
await globalState.appController.handleExit();
|
await appController.handleExit();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
menuItems.add(exitMenuItem);
|
menuItems.add(exitMenuItem);
|
||||||
final menu = Menu(items: menuItems);
|
final menu = Menu(items: menuItems);
|
||||||
await trayManager.setContextMenu(menu);
|
await trayManager.setContextMenu(menu);
|
||||||
if (Platform.isLinux) {
|
if (system.isLinux) {
|
||||||
await _updateSystemTray(
|
await _updateSystemTray(
|
||||||
brightness: trayState.brightness,
|
isStart: trayState.isStart,
|
||||||
force: focus,
|
tunEnable: trayState.tunEnable,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
updateTrayTitle(showTrayTitle: trayState.showTrayTitle, traffic: traffic);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateTrayTitle([Traffic? traffic]) async {
|
Future<void> updateTrayTitle({
|
||||||
// if (!system.isMacOS) {
|
required bool showTrayTitle,
|
||||||
// return;
|
required Traffic traffic,
|
||||||
// }
|
}) async {
|
||||||
// if (traffic == null) {
|
if (!system.isMacOS) {
|
||||||
// await trayManager.setTitle("");
|
return;
|
||||||
// } else {
|
}
|
||||||
// await trayManager.setTitle(
|
if (!showTrayTitle) {
|
||||||
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
|
await trayManager.setTitle('');
|
||||||
// );
|
} else {
|
||||||
// }
|
await trayManager.setTitle(traffic.trayTitle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _copyEnv(int port) async {
|
Future<void> _copyEnv(int port) async {
|
||||||
final url = 'http://127.0.0.1:$port';
|
final url = 'http://127.0.0.1:$port';
|
||||||
|
|
||||||
final cmdline =
|
final cmdline = system.isWindows
|
||||||
system.isWindows ? 'set \$env:all_proxy=$url' : 'export all_proxy=$url';
|
? 'set \$env:all_proxy=$url'
|
||||||
|
: 'export all_proxy=$url';
|
||||||
|
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(ClipboardData(text: cmdline));
|
||||||
ClipboardData(
|
|
||||||
text: cmdline,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
|
static Utils? _instance;
|
||||||
|
|
||||||
|
Utils._internal();
|
||||||
|
|
||||||
|
factory Utils() {
|
||||||
|
_instance ??= Utils._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
Color? getDelayColor(int? delay) {
|
Color? getDelayColor(int? delay) {
|
||||||
if (delay == null) return null;
|
if (delay == null) return null;
|
||||||
if (delay < 0) return Colors.red;
|
if (delay < 0) return Colors.red;
|
||||||
@@ -142,16 +151,9 @@ class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getTrayIconPath({required Brightness brightness}) {
|
String get traySuffix {
|
||||||
if (system.isMacOS) {
|
|
||||||
return 'assets/images/icon_white.png';
|
|
||||||
}
|
|
||||||
final suffix = system.isWindows ? 'ico' : 'png';
|
final suffix = system.isWindows ? 'ico' : 'png';
|
||||||
return 'assets/images/icon.$suffix';
|
return 'assets/images/icon/status_2.$suffix';
|
||||||
// return switch (brightness) {
|
|
||||||
// Brightness.dark => "assets/images/icon_white.$suffix",
|
|
||||||
// Brightness.light => "assets/images/icon_black.$suffix",
|
|
||||||
// };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int compareVersions(String version1, String version2) {
|
int compareVersions(String version1, String version2) {
|
||||||
@@ -326,7 +328,7 @@ class Utils {
|
|||||||
required Function function,
|
required Function function,
|
||||||
required void Function(T data, int elapsedMilliseconds) onWatch,
|
required void Function(T data, int elapsedMilliseconds) onWatch,
|
||||||
}) async {
|
}) async {
|
||||||
if (kDebugMode) {
|
if (kDebugMode && watchExecution) {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
final res = await function();
|
final res = await function();
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
@@ -335,6 +337,21 @@ class Utils {
|
|||||||
}
|
}
|
||||||
return await function();
|
return await function();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int fastHash(String string) {
|
||||||
|
var hash = 0xcbf29ce484222325;
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < string.length) {
|
||||||
|
final codeUnit = string.codeUnitAt(i++);
|
||||||
|
hash ^= codeUnit >> 8;
|
||||||
|
hash *= 0x100000001b3;
|
||||||
|
hash ^= codeUnit & 0xFF;
|
||||||
|
hash *= 0x100000001b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final utils = Utils();
|
final utils = Utils();
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
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/models/config.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:screen_retriever/screen_retriever.dart';
|
import 'package:screen_retriever/screen_retriever.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class Window {
|
class Window {
|
||||||
Future<void> init(int version) async {
|
static Window? _instance;
|
||||||
final props = globalState.config.windowProps;
|
|
||||||
|
Window._internal();
|
||||||
|
|
||||||
|
factory Window() {
|
||||||
|
_instance ??= Window._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> init(int version, WindowProps props) async {
|
||||||
final acquire = await singleInstanceLock.acquire();
|
final acquire = await singleInstanceLock.acquire();
|
||||||
if (!acquire) {
|
if (!acquire) {
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -19,13 +27,22 @@ class Window {
|
|||||||
protocol.register('flclash');
|
protocol.register('flclash');
|
||||||
}
|
}
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
|
// kDebugMode ? Size(680, 580) :
|
||||||
WindowOptions windowOptions = WindowOptions(
|
WindowOptions windowOptions = WindowOptions(
|
||||||
size: Size(props.width, props.height),
|
size: props.size,
|
||||||
minimumSize: const Size(380, 400),
|
minimumSize: const Size(380, 400),
|
||||||
);
|
);
|
||||||
if (!system.isMacOS || version > 10) {
|
if (!system.isMacOS || version > 10) {
|
||||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||||
}
|
}
|
||||||
|
await windowManager.setMaximizable(false);
|
||||||
|
await _windowPosition(props);
|
||||||
|
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
await windowManager.setPreventClose(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _windowPosition(WindowProps props) async {
|
||||||
if (!system.isMacOS) {
|
if (!system.isMacOS) {
|
||||||
final left = props.left ?? 0;
|
final left = props.left ?? 0;
|
||||||
final top = props.top ?? 0;
|
final top = props.top ?? 0;
|
||||||
@@ -50,9 +67,6 @@ class Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
|
||||||
await windowManager.setPreventClose(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> show() async {
|
Future<void> show() async {
|
||||||
@@ -69,6 +83,7 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
await windowManager.close();
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
lib/common/yaml.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:yaml_writer/yaml_writer.dart';
|
||||||
|
|
||||||
|
class Yaml {
|
||||||
|
static Yaml? _instance;
|
||||||
|
|
||||||
|
Yaml._internal();
|
||||||
|
|
||||||
|
factory Yaml() {
|
||||||
|
_instance ??= Yaml._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
String encode(Object? value) {
|
||||||
|
return YamlWriter().convert(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final yaml = Yaml();
|
||||||
1780
lib/controller.dart
@@ -1,14 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/core/core.dart';
|
import 'package:fl_clash/core/core.dart';
|
||||||
import 'package:fl_clash/core/interface.dart';
|
import 'package:fl_clash/core/interface.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/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
@@ -67,25 +65,23 @@ class CoreController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shutdown() async {
|
Future<void> shutdown(bool isUser) async {
|
||||||
await _interface.shutdown();
|
await _interface.shutdown(isUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<bool> get isInit => _interface.isInit;
|
FutureOr<bool> get isInit => _interface.isInit;
|
||||||
|
|
||||||
Future<String> validateConfig(String data) async {
|
Future<String> validateConfig(String path) async {
|
||||||
final path = await appPath.validateFilePath;
|
|
||||||
await globalState.genValidateFile(path, data);
|
|
||||||
final res = await _interface.validateConfig(path);
|
final res = await _interface.validateConfig(path);
|
||||||
await File(path).delete();
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> validateConfigFormBytes(Uint8List bytes) async {
|
Future<String> validateConfigWithData(String data) async {
|
||||||
final path = await appPath.validateFilePath;
|
final path = await appPath.tempFilePath;
|
||||||
await globalState.genValidateFileFormBytes(path, bytes);
|
final file = File(path);
|
||||||
|
await file.safeWriteAsString(data);
|
||||||
final res = await _interface.validateConfig(path);
|
final res = await _interface.validateConfig(path);
|
||||||
await File(path).delete();
|
await File(path).safeDelete();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +89,11 @@ class CoreController {
|
|||||||
return await _interface.updateConfig(updateParams);
|
return await _interface.updateConfig(updateParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> setupConfig(
|
Future<String> setupConfig({
|
||||||
ClashConfig clashConfig, {
|
required SetupParams params,
|
||||||
|
required SetupState setupState,
|
||||||
VoidCallback? preloadInvoke,
|
VoidCallback? preloadInvoke,
|
||||||
}) async {
|
}) async {
|
||||||
await globalState.genConfigFile(clashConfig);
|
|
||||||
final params = await globalState.getSetupParams();
|
|
||||||
final res = _interface.setupConfig(params);
|
final res = _interface.setupConfig(params);
|
||||||
if (preloadInvoke != null) {
|
if (preloadInvoke != null) {
|
||||||
preloadInvoke();
|
preloadInvoke();
|
||||||
@@ -109,36 +104,19 @@ class CoreController {
|
|||||||
Future<List<Group>> getProxiesGroups({
|
Future<List<Group>> getProxiesGroups({
|
||||||
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,
|
||||||
}) async {
|
}) async {
|
||||||
final proxies = await _interface.getProxies();
|
final proxiesData = await _interface.getProxies();
|
||||||
return Isolate.run<List<Group>>(() {
|
return toGroupsTask(
|
||||||
if (proxies.isEmpty) return [];
|
ComputeGroupsState(
|
||||||
final groupNames = [
|
proxiesData: proxiesData,
|
||||||
UsedProxy.GLOBAL.name,
|
|
||||||
...(proxies[UsedProxy.GLOBAL.name]['all'] as List).where((e) {
|
|
||||||
final proxy = proxies[e] ?? {};
|
|
||||||
return GroupTypeExtension.valueList.contains(proxy['type']);
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
final groupsRaw = groupNames.map((groupName) {
|
|
||||||
final group = proxies[groupName];
|
|
||||||
group['all'] = ((group['all'] ?? []) as List)
|
|
||||||
.map((name) => proxies[name])
|
|
||||||
.where((proxy) => proxy != null)
|
|
||||||
.toList();
|
|
||||||
return group;
|
|
||||||
}).toList();
|
|
||||||
final groups = groupsRaw.map((e) => Group.fromJson(e)).toList();
|
|
||||||
return computeSort(
|
|
||||||
groups: groups,
|
|
||||||
sortType: sortType,
|
sortType: sortType,
|
||||||
delayMap: delayMap,
|
delayMap: delayMap,
|
||||||
selectedMap: selectedMap,
|
selectedMap: selectedMap,
|
||||||
defaultTestUrl: defaultTestUrl,
|
defaultTestUrl: defaultTestUrl,
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
|
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
|
||||||
@@ -169,13 +147,11 @@ class CoreController {
|
|||||||
if (externalProvidersRawString.isEmpty) {
|
if (externalProvidersRawString.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Isolate.run<List<ExternalProvider>>(() {
|
final externalProviders =
|
||||||
final externalProviders =
|
(await externalProvidersRawString.commonToJSON<List<dynamic>>())
|
||||||
(json.decode(externalProvidersRawString) as List<dynamic>)
|
.map((item) => ExternalProvider.fromJson(item))
|
||||||
.map((item) => ExternalProvider.fromJson(item))
|
.toList();
|
||||||
.toList();
|
return externalProviders;
|
||||||
return externalProviders;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ExternalProvider?> getExternalProvider(
|
Future<ExternalProvider?> getExternalProvider(
|
||||||
@@ -221,11 +197,14 @@ class CoreController {
|
|||||||
return Delay.fromJson(json.decode(data));
|
return Delay.fromJson(json.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getConfig(String id) async {
|
Future<Map<String, dynamic>> getConfig(int id) async {
|
||||||
final profilePath = await appPath.getProfilePath(id);
|
final profilePath = await appPath.getProfilePath(id.toString());
|
||||||
final res = await _interface.getConfig(profilePath);
|
final res = await _interface.getConfig(profilePath);
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
return res.data;
|
final data = Map<String, dynamic>.from(res.data);
|
||||||
|
data['rules'] = data['rule'];
|
||||||
|
data.remove('rule');
|
||||||
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw res.message;
|
throw res.message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
@@ -12,7 +11,7 @@ mixin CoreInterface {
|
|||||||
|
|
||||||
Future<String> preload();
|
Future<String> preload();
|
||||||
|
|
||||||
Future<bool> shutdown();
|
Future<bool> shutdown(bool isUser);
|
||||||
|
|
||||||
Future<bool> get isInit;
|
Future<bool> get isInit;
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ mixin CoreInterface {
|
|||||||
|
|
||||||
Future<String> setupConfig(SetupParams setupParams);
|
Future<String> setupConfig(SetupParams setupParams);
|
||||||
|
|
||||||
Future<Map> getProxies();
|
Future<ProxiesData> getProxies();
|
||||||
|
|
||||||
Future<String> changeProxy(ChangeProxyParams changeProxyParams);
|
Future<String> changeProxy(ChangeProxyParams changeProxyParams);
|
||||||
|
|
||||||
@@ -86,14 +85,22 @@ abstract class CoreHandlerInterface with CoreInterface {
|
|||||||
dynamic data,
|
dynamic data,
|
||||||
Duration? timeout,
|
Duration? timeout,
|
||||||
}) async {
|
}) async {
|
||||||
await completer.future;
|
try {
|
||||||
if (kDebugMode) {
|
await completer.future.timeout(const Duration(seconds: 10));
|
||||||
|
} catch (e) {
|
||||||
|
commonPrint.log(
|
||||||
|
'Invoke pre ${method.name} timeout $e',
|
||||||
|
logLevel: LogLevel.error,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (kDebugMode && watchExecution) {
|
||||||
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
|
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.handleWatch(
|
return await utils.handleWatch(
|
||||||
function: () async {
|
function: () async {
|
||||||
return await invoke(method: method, data: data, timeout: timeout);
|
return await invoke<T>(method: method, data: data, timeout: timeout);
|
||||||
},
|
},
|
||||||
onWatch: (data, elapsedMilliseconds) {
|
onWatch: (data, elapsedMilliseconds) {
|
||||||
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
|
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
|
||||||
@@ -124,7 +131,7 @@ abstract class CoreHandlerInterface with CoreInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> shutdown();
|
Future<bool> shutdown(bool isUser);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> get isInit async {
|
Future<bool> get isInit async {
|
||||||
@@ -156,16 +163,15 @@ abstract class CoreHandlerInterface with CoreInterface {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result> getConfig(String path) async {
|
Future<Result> getConfig(String path) async {
|
||||||
return await _invoke<Result>(method: ActionMethod.getConfig, data: path) ??
|
final res = await _invoke(method: ActionMethod.getConfig, data: path);
|
||||||
Result.success({});
|
return res ?? Result.success({});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> setupConfig(SetupParams setupParams) async {
|
Future<String> setupConfig(SetupParams setupParams) async {
|
||||||
final data = await Isolate.run(() => json.encode(setupParams));
|
|
||||||
return await _invoke<String>(
|
return await _invoke<String>(
|
||||||
method: ActionMethod.setupConfig,
|
method: ActionMethod.setupConfig,
|
||||||
data: data,
|
data: json.encode(setupParams),
|
||||||
) ??
|
) ??
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
@@ -176,9 +182,13 @@ abstract class CoreHandlerInterface with CoreInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map> getProxies() async {
|
Future<ProxiesData> getProxies() async {
|
||||||
final map = await _invoke<Map>(method: ActionMethod.getProxies);
|
final data = await _invoke<Map<String, dynamic>>(
|
||||||
return map ?? {};
|
method: ActionMethod.getProxies,
|
||||||
|
);
|
||||||
|
return data != null
|
||||||
|
? ProxiesData.fromJson(data)
|
||||||
|
: ProxiesData(proxies: {}, all: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
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/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/core.dart';
|
import 'package:fl_clash/models/core.dart';
|
||||||
import 'package:fl_clash/plugins/service.dart';
|
import 'package:fl_clash/plugins/service.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
|
|
||||||
import 'interface.dart';
|
import 'interface.dart';
|
||||||
|
|
||||||
@@ -22,9 +22,7 @@ class CoreLib extends CoreHandlerInterface {
|
|||||||
return res ?? '';
|
return res ?? '';
|
||||||
}
|
}
|
||||||
_connectedCompleter.complete(true);
|
_connectedCompleter.complete(true);
|
||||||
final syncRes = await service?.syncAndroidState(
|
final syncRes = await service?.syncState(appController.sharedState);
|
||||||
globalState.getAndroidState(),
|
|
||||||
);
|
|
||||||
return syncRes ?? '';
|
return syncRes ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +37,12 @@ class CoreLib extends CoreHandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> shutdown() async {
|
Future<bool> shutdown(_) async {
|
||||||
await service?.shutdown();
|
if (!_connectedCompleter.isCompleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
_connectedCompleter = Completer();
|
_connectedCompleter = Completer();
|
||||||
return true;
|
return service?.shutdown() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
|
|
||||||
Completer<Socket> _socketCompleter = Completer();
|
Completer<Socket> _socketCompleter = Completer();
|
||||||
|
|
||||||
|
Completer<bool> _shutdownCompleter = Completer();
|
||||||
|
|
||||||
final Map<String, Completer> _callbackCompleterMap = {};
|
final Map<String, Completer> _callbackCompleterMap = {};
|
||||||
|
|
||||||
Process? _process;
|
Process? _process;
|
||||||
@@ -35,6 +37,9 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
if (result.id?.isEmpty == true) {
|
if (result.id?.isEmpty == true) {
|
||||||
coreEventManager.sendEvent(CoreEvent.fromJson(result.data));
|
coreEventManager.sendEvent(CoreEvent.fromJson(result.data));
|
||||||
}
|
}
|
||||||
|
if (completer?.isCompleted == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
completer?.complete(data);
|
completer?.complete(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +75,15 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
.transform(uint8ListToListIntConverter)
|
.transform(uint8ListToListIntConverter)
|
||||||
.transform(utf8.decoder)
|
.transform(utf8.decoder)
|
||||||
.transform(LineSplitter())
|
.transform(LineSplitter())
|
||||||
.listen((data) {
|
.listen((data) async {
|
||||||
handleResult(ActionResult.fromJson(json.decode(data.trim())));
|
final dataJson = await data.trim().commonToJSON<dynamic>();
|
||||||
|
handleResult(ActionResult.fromJson(dataJson));
|
||||||
})
|
})
|
||||||
.onDone(() {
|
.onDone(() {
|
||||||
_handleInvokeCrashEvent();
|
_handleInvokeCrashEvent();
|
||||||
|
if (!_shutdownCompleter.isCompleted) {
|
||||||
|
_shutdownCompleter.complete(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +95,7 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
if (_process != null) {
|
if (_process != null) {
|
||||||
await shutdown();
|
await shutdown(false);
|
||||||
}
|
}
|
||||||
final serverSocket = await _serverCompleter.future;
|
final serverSocket = await _serverCompleter.future;
|
||||||
final arg = system.isWindows
|
final arg = system.isWindows
|
||||||
@@ -112,6 +121,7 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
@override
|
@override
|
||||||
destroy() async {
|
destroy() async {
|
||||||
final server = await _serverCompleter.future;
|
final server = await _serverCompleter.future;
|
||||||
|
await shutdown(false);
|
||||||
await server.close();
|
await server.close();
|
||||||
await _deleteSocketFile();
|
await _deleteSocketFile();
|
||||||
return true;
|
return true;
|
||||||
@@ -125,9 +135,7 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
Future<void> _deleteSocketFile() async {
|
Future<void> _deleteSocketFile() async {
|
||||||
if (!system.isWindows) {
|
if (!system.isWindows) {
|
||||||
final file = File(unixSocketPath);
|
final file = File(unixSocketPath);
|
||||||
if (await file.exists()) {
|
await file.safeDelete();
|
||||||
await file.delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +143,16 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
if (_socketCompleter.isCompleted) {
|
if (_socketCompleter.isCompleted) {
|
||||||
final socket = await _socketCompleter.future;
|
final socket = await _socketCompleter.future;
|
||||||
_socketCompleter = Completer();
|
_socketCompleter = Completer();
|
||||||
socket.close();
|
await socket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
shutdown() async {
|
shutdown(bool isUser) async {
|
||||||
|
if (!_socketCompleter.isCompleted && _process == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_shutdownCompleter = Completer();
|
||||||
await _destroySocket();
|
await _destroySocket();
|
||||||
_clearCompleter();
|
_clearCompleter();
|
||||||
if (system.isWindows) {
|
if (system.isWindows) {
|
||||||
@@ -148,7 +160,11 @@ class CoreService extends CoreHandlerInterface {
|
|||||||
}
|
}
|
||||||
_process?.kill();
|
_process?.kill();
|
||||||
_process = null;
|
_process = null;
|
||||||
return true;
|
if (isUser) {
|
||||||
|
return _shutdownCompleter.future;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearCompleter() {
|
void _clearCompleter() {
|
||||||
|
|||||||
78
lib/database/database.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
|
||||||
|
part 'generated/database.g.dart';
|
||||||
|
part 'links.dart';
|
||||||
|
part 'profiles.dart';
|
||||||
|
part 'rules.dart';
|
||||||
|
part 'scripts.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(
|
||||||
|
tables: [Profiles, Scripts, Rules, ProfileRuleLinks],
|
||||||
|
daos: [ProfilesDao, ScriptsDao, RulesDao],
|
||||||
|
)
|
||||||
|
class Database extends _$Database {
|
||||||
|
Database([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
static LazyDatabase _openConnection() {
|
||||||
|
return LazyDatabase(() async {
|
||||||
|
final databaseFile = File(await appPath.databasePath);
|
||||||
|
return NativeDatabase.createInBackground(databaseFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> restore(
|
||||||
|
List<Profile> profiles,
|
||||||
|
List<Script> scripts,
|
||||||
|
List<Rule> rules,
|
||||||
|
List<ProfileRuleLink> links, {
|
||||||
|
bool isOverride = false,
|
||||||
|
}) async {
|
||||||
|
if (profiles.isNotEmpty ||
|
||||||
|
scripts.isNotEmpty ||
|
||||||
|
rules.isNotEmpty ||
|
||||||
|
links.isNotEmpty) {
|
||||||
|
await batch((b) {
|
||||||
|
isOverride
|
||||||
|
? profilesDao.setAllWithBatch(b, profiles)
|
||||||
|
: profilesDao.putAllWithBatch(
|
||||||
|
b,
|
||||||
|
profiles.map((item) => item.toCompanion()),
|
||||||
|
);
|
||||||
|
scriptsDao.setAllWithBatch(b, scripts);
|
||||||
|
rulesDao.restoreWithBatch(b, rules, links);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TableInfoExt<Tbl extends Table, Row> on TableInfo<Tbl, Row> {
|
||||||
|
void setAll(
|
||||||
|
Batch batch,
|
||||||
|
Iterable<Insertable<Row>> items, {
|
||||||
|
required Expression<bool> Function(Tbl tbl) deleteFilter,
|
||||||
|
}) async {
|
||||||
|
batch.insertAllOnConflictUpdate(this, items);
|
||||||
|
batch.deleteWhere(this, deleteFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> remove(Expression<bool> Function(Tbl tbl) filter) async {
|
||||||
|
return await (delete()..where(filter)).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> put(Insertable<Row> item) async {
|
||||||
|
return await insertOnConflictUpdate(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final database = Database();
|
||||||
2941
lib/database/generated/database.g.dart
Normal file
52
lib/database/links.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
part of 'database.dart';
|
||||||
|
|
||||||
|
@DataClassName('RawProfileRuleLink')
|
||||||
|
@TableIndex(
|
||||||
|
name: 'idx_profile_scene_order',
|
||||||
|
columns: {#profileId, #scene, #order},
|
||||||
|
)
|
||||||
|
class ProfileRuleLinks extends Table {
|
||||||
|
@override
|
||||||
|
String get tableName => 'profile_rule_mapping';
|
||||||
|
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
IntColumn get profileId => integer().nullable().references(
|
||||||
|
Profiles,
|
||||||
|
#id,
|
||||||
|
onDelete: KeyAction.cascade,
|
||||||
|
)();
|
||||||
|
|
||||||
|
IntColumn get ruleId =>
|
||||||
|
integer().references(Rules, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get scene => textEnum<RuleScene>().nullable()();
|
||||||
|
|
||||||
|
TextColumn get order => text().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RawProfileRuleLinkExt on RawProfileRuleLink {
|
||||||
|
ProfileRuleLink toLink() {
|
||||||
|
return ProfileRuleLink(
|
||||||
|
profileId: profileId,
|
||||||
|
ruleId: ruleId,
|
||||||
|
scene: scene,
|
||||||
|
order: order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileRuleLinksCompanionExt on ProfileRuleLink {
|
||||||
|
ProfileRuleLinksCompanion toCompanion() {
|
||||||
|
return ProfileRuleLinksCompanion.insert(
|
||||||
|
id: key,
|
||||||
|
ruleId: ruleId,
|
||||||
|
scene: Value(scene),
|
||||||
|
profileId: Value(profileId),
|
||||||
|
order: Value(order),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||