Compare commits
4 Commits
v0.8.90-pr
...
v0.8.92-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
755c66e5b0 | ||
|
|
6e404ab19c | ||
|
|
2395a4b20c | ||
|
|
d3c3f04062 |
110
.github/workflows/build.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
os: ubuntu-22.04
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-13
|
||||
os: macos-15-intel
|
||||
arch: amd64
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
@@ -64,22 +64,25 @@ jobs:
|
||||
cache-dependency-path: |
|
||||
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
|
||||
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
|
||||
uses: subosito/flutter-action@v2
|
||||
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
|
||||
# flutter-version: 3.29.3
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
run: |
|
||||
flutter --version
|
||||
flutter pub get
|
||||
|
||||
- name: Setup
|
||||
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
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
||||
currentTag=""
|
||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||
if (( i < ${#tags[@]} )); then
|
||||
tag=${tags[$i]}
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
echo "## $currentTag" >> NEW_CHANGELOG.md
|
||||
echo "" >> NEW_CHANGELOG.md
|
||||
if [ -n "$tag" ]; then
|
||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
||||
else
|
||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
||||
fi
|
||||
echo "" >> NEW_CHANGELOG.md
|
||||
fi
|
||||
currentTag=$tag
|
||||
last_ver=$(grep -m1 '^## ' CHANGELOG.md 2>/dev/null | sed 's/^## //')
|
||||
|
||||
tags=($(git tag --merged HEAD --sort=-creatordate))
|
||||
|
||||
temp="NEW_CHANGELOG.md" > "$temp"
|
||||
|
||||
for i in "${!tags[@]}"; do
|
||||
curr="${tags[i]}"
|
||||
[[ "$curr" == "$last_ver" ]] && break
|
||||
|
||||
prev="${tags[i+1]}"
|
||||
range="${prev:+$prev..}$curr"
|
||||
|
||||
echo -e "## $curr\n" >> "$temp"
|
||||
git log --no-merges --pretty=format:"%B" "$range" | \
|
||||
awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$temp"
|
||||
done
|
||||
cat CHANGELOG.md >> NEW_CHANGELOG.md
|
||||
cat NEW_CHANGELOG.md > CHANGELOG.md
|
||||
[ -f CHANGELOG.md ] && cat CHANGELOG.md >> "$temp"
|
||||
|
||||
mv "$temp" CHANGELOG.md
|
||||
|
||||
- name: Commit
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
@@ -181,31 +176,24 @@ jobs:
|
||||
|
||||
- name: Generate release.md
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
|
||||
currentTag=""
|
||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||
if (( i < ${#tags[@]} )); then
|
||||
tag=${tags[$i]}
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ -n "$tag" ]; then
|
||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
|
||||
else
|
||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
|
||||
fi
|
||||
echo "" >> release.md
|
||||
fi
|
||||
currentTag=$tag
|
||||
tags=($(git tag --merged HEAD --sort=-creatordate))
|
||||
preTag=$(curl -s "https://api.github.com/repos/chen08209/FlClash/releases/latest" | \
|
||||
sed -nE 's/.*"tag_name": "([^"]+)".*/\1/p')
|
||||
|
||||
[ -z "$preTag" ] && preTag=""
|
||||
|
||||
out="release.md" > "$out"
|
||||
|
||||
for i in "${!tags[@]}"; do
|
||||
curr="${tags[i]}"
|
||||
[[ "$curr" == "$preTag" ]] && break
|
||||
|
||||
prev="${tags[i+1]}"
|
||||
range="${prev:+$prev..}$curr"
|
||||
|
||||
git log --no-merges --pretty=format:"%B" "$range" | \
|
||||
awk '!/Update changelog/ && NF {print "- " $0 "\n"}' >> "$out"
|
||||
done
|
||||
|
||||
- name: Push to telegram
|
||||
env:
|
||||
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
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
@@ -41,6 +41,11 @@ app.*.symbols
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
#AI generated
|
||||
CLAUDE.md
|
||||
/.claude
|
||||
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
@@ -53,7 +58,6 @@ app.*.map.json
|
||||
/android/core/**/cmake-build-*/
|
||||
/android/core/**/jniLibs/
|
||||
|
||||
|
||||
#FlClash
|
||||
/libclash/
|
||||
/android/app/src/main/jniLibs/
|
||||
@@ -61,3 +65,6 @@ app.*.map.json
|
||||
/macos/**/Package.resolved
|
||||
devtools_options.yaml
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
.fvmrc
|
||||
4
.gitmodules
vendored
@@ -6,5 +6,9 @@
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
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.
|
||||
# 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:
|
||||
revision: 796c8ef79279f9c774545b3771238c3098dbefab
|
||||
channel: stable
|
||||
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
@@ -13,26 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
|
||||
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
|
||||
- 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
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: windows
|
||||
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
|
||||
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
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
|
||||
|
||||
- Fix some issues
|
||||
|
||||
@@ -64,16 +64,17 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
applicationIdSuffix = ".debug"
|
||||
applicationIdSuffix = ".dev"
|
||||
}
|
||||
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = if (isRelease) {
|
||||
signingConfigs.getByName("release")
|
||||
if (isRelease) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
applicationIdSuffix = ".dev"
|
||||
}
|
||||
|
||||
proguardFiles(
|
||||
|
||||
@@ -41,6 +41,25 @@
|
||||
"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
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
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.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -21,6 +26,30 @@ import kotlin.coroutines.resume
|
||||
|
||||
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 =
|
||||
withContext(Dispatchers.IO) {
|
||||
val cacheDir = GlobalState.application.cacheDir
|
||||
@@ -118,4 +147,4 @@ fun <T> MethodChannel.invokeMethodOnMainThread(
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
@@ -18,9 +17,6 @@ class MainActivity : FlutterActivity(),
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
State.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.follow.clash
|
||||
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.common.ServiceDelegate
|
||||
import com.follow.clash.common.formatString
|
||||
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.IRemoteInterface
|
||||
import com.follow.clash.service.IResultInterface
|
||||
import com.follow.clash.service.IVoidInterface
|
||||
import com.follow.clash.service.RemoteService
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
@@ -40,7 +42,7 @@ object Service {
|
||||
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>()
|
||||
return delegate.useService {
|
||||
it.invokeAction(
|
||||
@@ -51,13 +53,50 @@ object Service {
|
||||
res.add(result ?: byteArrayOf())
|
||||
ack?.onAck()
|
||||
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(
|
||||
cb: ((result: String?) -> Unit)?
|
||||
): Result<Unit> {
|
||||
@@ -65,24 +104,24 @@ object Service {
|
||||
return delegate.useService {
|
||||
it.setEventListener(
|
||||
when (cb != null) {
|
||||
true -> object : IEventInterface.Stub() {
|
||||
override fun onEvent(
|
||||
id: String, data: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
|
||||
) {
|
||||
if (results[id] == null) {
|
||||
results[id] = mutableListOf()
|
||||
}
|
||||
results[id]?.add(data ?: byteArrayOf())
|
||||
ack?.onAck()
|
||||
if (isSuccess) {
|
||||
cb(results[id]?.formatString())
|
||||
results.remove(id)
|
||||
true -> object : IEventInterface.Stub() {
|
||||
override fun onEvent(
|
||||
id: String, data: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
|
||||
) {
|
||||
if (results[id] == null) {
|
||||
results[id] = mutableListOf()
|
||||
}
|
||||
results[id]?.add(data ?: byteArrayOf())
|
||||
ack?.onAck()
|
||||
if (isSuccess) {
|
||||
cb(results[id]?.formatString())
|
||||
results.remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false -> null
|
||||
})
|
||||
false -> null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +155,7 @@ object Service {
|
||||
try {
|
||||
block(callback)
|
||||
} catch (e: Exception) {
|
||||
GlobalState.log("awaitIResultInterface $e")
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.net.VpnService
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.models.SharedState
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
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.dart.DartExecutor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
enum class RunState {
|
||||
START, PENDING, STOP
|
||||
@@ -25,20 +24,17 @@ object State {
|
||||
|
||||
var runTime: Long = 0
|
||||
|
||||
var sharedState: SharedState = SharedState()
|
||||
|
||||
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
|
||||
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
var serviceFlutterEngine: FlutterEngine? = null
|
||||
|
||||
val appPlugin: AppPlugin?
|
||||
get() = flutterEngine?.plugin<AppPlugin>() ?: serviceFlutterEngine?.plugin<AppPlugin>()
|
||||
|
||||
val servicePlugin: ServicePlugin?
|
||||
get() = flutterEngine?.plugin<ServicePlugin>()
|
||||
?: serviceFlutterEngine?.plugin<ServicePlugin>()
|
||||
get() = flutterEngine?.plugin<AppPlugin>()
|
||||
|
||||
val tilePlugin: TilePlugin?
|
||||
get() = flutterEngine?.plugin<TilePlugin>() ?: serviceFlutterEngine?.plugin<TilePlugin>()
|
||||
get() = flutterEngine?.plugin<TilePlugin>()
|
||||
|
||||
suspend fun handleToggleAction() {
|
||||
var action: (suspend () -> Unit)?
|
||||
@@ -54,13 +50,17 @@ object State {
|
||||
|
||||
suspend fun handleSyncState() {
|
||||
runLock.withLock {
|
||||
Service.bind()
|
||||
runTime = Service.getRunTime()
|
||||
val runState = when (runTime == 0L) {
|
||||
true -> RunState.STOP
|
||||
false -> RunState.START
|
||||
try {
|
||||
Service.bind()
|
||||
runTime = Service.getRunTime()
|
||||
val runState = when (runTime == 0L) {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
startServiceWithEngine()
|
||||
startServiceWithPref()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -84,16 +84,18 @@ object State {
|
||||
return
|
||||
}
|
||||
tilePlugin?.handleStop()
|
||||
if (flutterEngine != null || serviceFlutterEngine != null) {
|
||||
if (flutterEngine != null) {
|
||||
return
|
||||
}
|
||||
GlobalState.application.showToast(sharedState.stopTip)
|
||||
handleStopService()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleStartService() {
|
||||
val appPlugin = flutterEngine?.plugin<AppPlugin>()
|
||||
if (appPlugin != null) {
|
||||
appPlugin?.requestNotificationsPermission {
|
||||
appPlugin.requestNotificationsPermission {
|
||||
startService()
|
||||
}
|
||||
return
|
||||
@@ -101,75 +103,101 @@ object State {
|
||||
startService()
|
||||
}
|
||||
|
||||
fun handleStopService() {
|
||||
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() {
|
||||
private fun startServiceWithPref() {
|
||||
GlobalState.launch {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value != RunState.STOP) {
|
||||
return@launch
|
||||
}
|
||||
GlobalState.log("Create service engine")
|
||||
withContext(Dispatchers.Main) {
|
||||
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)
|
||||
}
|
||||
sharedState = GlobalState.application.sharedState
|
||||
setupAndStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
GlobalState.launch {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value != RunState.STOP) {
|
||||
return@launch
|
||||
}
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
if (servicePlugin == null) {
|
||||
return@launch
|
||||
}
|
||||
val options = servicePlugin?.handleGetVpnOptions()
|
||||
if (options == null) {
|
||||
return@launch
|
||||
}
|
||||
appPlugin?.prepare(options.enable) {
|
||||
runTime = Service.startService(options, runTime)
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
try {
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
val options = sharedState.vpnOptions ?: return@launch
|
||||
appPlugin?.let {
|
||||
it.prepare(options.enable) {
|
||||
runTime = Service.startService(options, runTime)
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
}
|
||||
} ?: run {
|
||||
val intent = VpnService.prepare(GlobalState.application)
|
||||
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
|
||||
|
||||
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 currentProfileName: String = "FlClash",
|
||||
val stopText: String = "Stop",
|
||||
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.net.VpnService
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
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.getPackageIconPath
|
||||
import com.follow.clash.models.Package
|
||||
import com.follow.clash.showToast
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
@@ -193,7 +193,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private fun tip(message: String?) {
|
||||
Toast.makeText(GlobalState.application, message, Toast.LENGTH_LONG).show()
|
||||
GlobalState.application.showToast(message)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
||||
@@ -3,13 +3,9 @@ package com.follow.clash.plugins
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.Service
|
||||
import com.follow.clash.State
|
||||
import com.follow.clash.awaitResult
|
||||
import com.follow.clash.common.Components
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.invokeMethodOnMainThread
|
||||
import com.follow.clash.models.AppState
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import com.follow.clash.models.SharedState
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
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) {
|
||||
"init" -> {
|
||||
handleInit(call, result)
|
||||
handleInit(result)
|
||||
}
|
||||
|
||||
"shutdown" -> {
|
||||
@@ -94,11 +90,6 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
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)
|
||||
|
||||
fun handleSendEvent(value: String?) {
|
||||
@@ -116,31 +107,19 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) {
|
||||
val data = call.arguments<String>()!!
|
||||
val params = Gson().fromJson(data, AppState::class.java)
|
||||
GlobalState.setCrashlytics(params.crashlytics)
|
||||
State.sharedState = Gson().fromJson(data, SharedState::class.java)
|
||||
launch {
|
||||
Service.updateNotificationParams(
|
||||
NotificationParams(
|
||||
title = params.currentProfileName,
|
||||
stopText = params.stopText,
|
||||
onlyStatisticsProxy = params.onlyStatisticsProxy
|
||||
)
|
||||
)
|
||||
Service.setCrashlytics(params.crashlytics)
|
||||
State.syncState()
|
||||
result.success("")
|
||||
}
|
||||
}
|
||||
|
||||
fun handleInit(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
||||
fun handleInit(result: MethodChannel.Result) {
|
||||
Service.bind()
|
||||
launch {
|
||||
val needSetEventListener = call.arguments<Boolean>() ?: false
|
||||
when (needSetEventListener) {
|
||||
true -> Service.setEventListener {
|
||||
handleSendEvent(it)
|
||||
}
|
||||
|
||||
false -> Service.setEventListener(null)
|
||||
Service.setEventListener {
|
||||
handleSendEvent(it)
|
||||
}.onSuccess {
|
||||
result.success("")
|
||||
}.onFailure {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="240"
|
||||
android:viewportHeight="240">
|
||||
<group android:scaleX="0.924"
|
||||
android:scaleY="0.924"
|
||||
android:translateX="9.12"
|
||||
android:translateY="9.12">
|
||||
<group android:scaleX="0.63461536"
|
||||
android:scaleY="0.63461536"
|
||||
android:translateX="45.96154"
|
||||
android:translateY="43.846153">
|
||||
<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:fillColor="#6666FB"/>
|
||||
<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:fillColor="#336AB6"/>
|
||||
<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:fillColor="#5CA8E9"/>
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="240"
|
||||
android:viewportHeight="240">
|
||||
<group android:scaleX="0.924"
|
||||
android:scaleY="0.924"
|
||||
android:translateX="9.12"
|
||||
android:translateY="9.12">
|
||||
<group android:scaleX="0.63461536"
|
||||
android:scaleY="0.63461536"
|
||||
android:translateX="45.96154"
|
||||
android:translateY="43.846153">
|
||||
<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:fillColor="#6666FB"/>
|
||||
<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:fillColor="#336AB6"/>
|
||||
<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:fillColor="#5CA8E9"/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.build.kotlin)
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library") apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
@@ -31,4 +22,3 @@ subprojects {
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,14 @@ Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean su
|
||||
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_resolve_process;
|
||||
@@ -99,12 +107,12 @@ call_tun_interface_resolve_process_impl(void *tun_interface, const int protocol,
|
||||
const int uid) {
|
||||
ATTACH_JNI();
|
||||
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||
static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_process,
|
||||
protocol,
|
||||
new_string(source),
|
||||
new_string(target),
|
||||
uid));
|
||||
static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_process,
|
||||
protocol,
|
||||
new_string(source),
|
||||
new_string(target),
|
||||
uid));
|
||||
return get_string(packageName);
|
||||
}
|
||||
|
||||
@@ -191,4 +199,10 @@ extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
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 getTraffic(onlyStatisticsProxy: Boolean): String
|
||||
|
||||
@@ -4,11 +4,13 @@ package com.follow.clash.service;
|
||||
import com.follow.clash.service.ICallbackInterface;
|
||||
import com.follow.clash.service.IEventInterface;
|
||||
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.NotificationParams;
|
||||
|
||||
interface IRemoteInterface {
|
||||
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 startService(in VpnOptions options, in long runTime, 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() {
|
||||
loader.load()
|
||||
try {
|
||||
loader.load()
|
||||
} catch (_: Exception) {
|
||||
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?) {
|
||||
State.notificationParamsFlow.tryEmit(params)
|
||||
}
|
||||
@@ -108,6 +137,7 @@ class RemoteService : Service(),
|
||||
runtime: Long,
|
||||
result: IResultInterface,
|
||||
) {
|
||||
GlobalState.log("remote startService")
|
||||
State.options = options
|
||||
handleStartService(runtime, result)
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ class VpnService : SystemVpnService(), IBaseService,
|
||||
addDnsServer(DNS6)
|
||||
}
|
||||
setMtu(9000)
|
||||
options.accessControl.let { accessControl ->
|
||||
options.accessControlProps.let { accessControl ->
|
||||
if (accessControl.enable) {
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.ACCEPT_SELECTED -> {
|
||||
@@ -234,9 +234,13 @@ class VpnService : SystemVpnService(), IBaseService,
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
loader.load()
|
||||
State.options?.let {
|
||||
handleStart(it)
|
||||
try {
|
||||
loader.load()
|
||||
State.options?.let {
|
||||
handleStart(it)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
|
||||
import java.net.InetAddress
|
||||
|
||||
@Parcelize
|
||||
data class AccessControl(
|
||||
data class AccessControlProps(
|
||||
val enable: Boolean,
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
@@ -19,7 +19,7 @@ data class VpnOptions(
|
||||
val port: Int,
|
||||
val ipv6: Boolean,
|
||||
val dnsHijacking: Boolean,
|
||||
val accessControl: AccessControl,
|
||||
val accessControlProps: AccessControlProps,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
|
||||
@@ -129,14 +129,8 @@
|
||||
"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.",
|
||||
"tip": "tip",
|
||||
"backupAndRecovery": "Backup and Recovery",
|
||||
"backupAndRecoveryDesc": "Sync data via WebDAV or file",
|
||||
"account": "Account",
|
||||
"backup": "Backup",
|
||||
"recovery": "Recovery",
|
||||
"recoveryProfiles": "Only recovery profiles",
|
||||
"recoveryAll": "Recovery all data",
|
||||
"recoverySuccess": "Recovery success",
|
||||
"backupSuccess": "Backup success",
|
||||
"noInfo": "No info",
|
||||
"pleaseBindWebDAV": "Please bind WebDAV",
|
||||
@@ -219,9 +213,7 @@
|
||||
"local": "Local",
|
||||
"remote": "Remote",
|
||||
"remoteBackupDesc": "Backup local data to WebDAV",
|
||||
"remoteRecoveryDesc": "Recovery data from WebDAV",
|
||||
"localBackupDesc": "Backup local data to local",
|
||||
"localRecoveryDesc": "Recovery data from file",
|
||||
"mode": "Mode",
|
||||
"time": "Time",
|
||||
"source": "Source",
|
||||
@@ -340,6 +332,8 @@
|
||||
"none": "none",
|
||||
"basicConfig": "Basic configuration",
|
||||
"basicConfigDesc": "Modify the basic configuration globally",
|
||||
"advancedConfig": "Advanced configuration",
|
||||
"advancedConfigDesc": "Provide diverse configuration options",
|
||||
"selectedCountTitle": "{count} items have been selected",
|
||||
"addRule": "Add rule",
|
||||
"ruleName": "Rule name",
|
||||
@@ -379,9 +373,9 @@
|
||||
"systemApp": "System APP",
|
||||
"noNetworkApp": "No network APP",
|
||||
"contactMe": "Contact me",
|
||||
"recoveryStrategy": "Recovery strategy",
|
||||
"recoveryStrategy_override": "Override",
|
||||
"recoveryStrategy_compatible": "Compatible",
|
||||
"restoreStrategy": "Restore strategy",
|
||||
"restoreStrategy_override": "Override",
|
||||
"restoreStrategy_compatible": "Compatible",
|
||||
"logsTest": "Logs test",
|
||||
"emptyTip": "{label} cannot be empty",
|
||||
"urlTip": "{label} must be a url",
|
||||
@@ -390,7 +384,7 @@
|
||||
"existsTip": "Current {label} already exists",
|
||||
"deleteTip": "Are you sure you want to delete the current {label}?",
|
||||
"deleteMultipTip": "Are you sure you want to delete the selected {label}?",
|
||||
"nullTip": "No {label} at the moment",
|
||||
"nullTip": "No {label} yet",
|
||||
"script": "Script",
|
||||
"color": "Color",
|
||||
"rename": "Rename",
|
||||
@@ -434,5 +428,52 @@
|
||||
"crashlytics": "Crash Analysis",
|
||||
"crashlyticsTip": "When enabled, automatically uploads crash logs without sensitive information when the app crashes",
|
||||
"appendSystemDns": "Append System DNS",
|
||||
"appendSystemDnsTip": "Forcefully append system DNS to the configuration"
|
||||
"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"
|
||||
}
|
||||
@@ -129,14 +129,8 @@
|
||||
"compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
|
||||
"notSelectedTip": "現在のプロキシグループは選択できません",
|
||||
"tip": "ヒント",
|
||||
"backupAndRecovery": "バックアップと復元",
|
||||
"backupAndRecoveryDesc": "WebDAVまたはファイルでデータを同期",
|
||||
"account": "アカウント",
|
||||
"backup": "バックアップ",
|
||||
"recovery": "復元",
|
||||
"recoveryProfiles": "プロファイルのみ復元",
|
||||
"recoveryAll": "全データ復元",
|
||||
"recoverySuccess": "復元成功",
|
||||
"backupSuccess": "バックアップ成功",
|
||||
"noInfo": "情報なし",
|
||||
"pleaseBindWebDAV": "WebDAVをバインドしてください",
|
||||
@@ -219,9 +213,7 @@
|
||||
"local": "ローカル",
|
||||
"remote": "リモート",
|
||||
"remoteBackupDesc": "WebDAVにデータをバックアップ",
|
||||
"remoteRecoveryDesc": "WebDAVからデータを復元",
|
||||
"localBackupDesc": "ローカルにデータをバックアップ",
|
||||
"localRecoveryDesc": "ファイルからデータを復元",
|
||||
"mode": "モード",
|
||||
"time": "時間",
|
||||
"source": "ソース",
|
||||
@@ -340,6 +332,8 @@
|
||||
"none": "なし",
|
||||
"basicConfig": "基本設定",
|
||||
"basicConfigDesc": "基本設定をグローバルに変更",
|
||||
"advancedConfig": "高度な設定",
|
||||
"advancedConfigDesc": "多様な設定を提供",
|
||||
"selectedCountTitle": "{count} 項目が選択されています",
|
||||
"addRule": "ルールを追加",
|
||||
"ruleName": "ルール名",
|
||||
@@ -380,9 +374,9 @@
|
||||
"systemApp": "システムアプリ",
|
||||
"noNetworkApp": "ネットワークなしアプリ",
|
||||
"contactMe": "連絡する",
|
||||
"recoveryStrategy": "リカバリー戦略",
|
||||
"recoveryStrategy_override": "オーバーライド",
|
||||
"recoveryStrategy_compatible": "互換性",
|
||||
"restoreStrategy": "復元ストラテジー",
|
||||
"restoreStrategy_override": "上書き",
|
||||
"restoreStrategy_compatible": "互換",
|
||||
"logsTest": "ログテスト",
|
||||
"emptyTip": "{label}は空欄にできません",
|
||||
"urlTip": "{label}はURLである必要があります",
|
||||
@@ -391,7 +385,7 @@
|
||||
"existsTip": "現在の{label}は既に存在しています",
|
||||
"deleteTip": "現在の{label}を削除してもよろしいですか?",
|
||||
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
|
||||
"nullTip": "現在{label}はありません",
|
||||
"nullTip": "まだ{label}はありません",
|
||||
"script": "スクリプト",
|
||||
"color": "カラー",
|
||||
"rename": "リネーム",
|
||||
@@ -435,5 +429,52 @@
|
||||
"crashlytics": "クラッシュ分析",
|
||||
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
|
||||
"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": "プロファイルを追加"
|
||||
}
|
||||
@@ -340,6 +340,8 @@
|
||||
"none": "Нет",
|
||||
"basicConfig": "Базовая конфигурация",
|
||||
"basicConfigDesc": "Глобальное изменение базовых настроек",
|
||||
"advancedConfig": "Расширенная конфигурация",
|
||||
"advancedConfigDesc": "Предоставляет разнообразные варианты конфигурации",
|
||||
"selectedCountTitle": "Выбрано {count} элементов",
|
||||
"addRule": "Добавить правило",
|
||||
"ruleName": "Название правила",
|
||||
@@ -380,9 +382,9 @@
|
||||
"systemApp": "Системное приложение",
|
||||
"noNetworkApp": "Приложение без сети",
|
||||
"contactMe": "Свяжитесь со мной",
|
||||
"recoveryStrategy": "Стратегия восстановления",
|
||||
"recoveryStrategy_override": "Переопределение",
|
||||
"recoveryStrategy_compatible": "Совместимый",
|
||||
"restoreStrategy": "Стратегия восстановления",
|
||||
"restoreStrategy_override": "Перезаписать",
|
||||
"restoreStrategy_compatible": "Совместимый",
|
||||
"logsTest": "Тест журналов",
|
||||
"emptyTip": "{label} не может быть пустым",
|
||||
"urlTip": "{label} должен быть URL",
|
||||
@@ -391,7 +393,7 @@
|
||||
"existsTip": "Текущий {label} уже существует",
|
||||
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
|
||||
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
|
||||
"nullTip": "Сейчас {label} нет",
|
||||
"nullTip": "{label} пока отсутствуют",
|
||||
"script": "Скрипт",
|
||||
"color": "Цвет",
|
||||
"rename": "Переименовать",
|
||||
@@ -435,5 +437,52 @@
|
||||
"crashlytics": "Анализ сбоев",
|
||||
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
|
||||
"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": "Добавить профиль"
|
||||
}
|
||||
@@ -129,14 +129,8 @@
|
||||
"compatibleDesc": "开启将失去部分应用能力,获得全量的Clash的支持",
|
||||
"notSelectedTip": "当前代理组无法选中",
|
||||
"tip": "提示",
|
||||
"backupAndRecovery": "备份与恢复",
|
||||
"backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
|
||||
"account": "账号",
|
||||
"backup": "备份",
|
||||
"recovery": "恢复",
|
||||
"recoveryProfiles": "仅恢复配置文件",
|
||||
"recoveryAll": "恢复所有数据",
|
||||
"recoverySuccess": "恢复成功",
|
||||
"backupSuccess": "备份成功",
|
||||
"noInfo": "暂无信息",
|
||||
"pleaseBindWebDAV": "请绑定WebDAV",
|
||||
@@ -219,9 +213,7 @@
|
||||
"local": "本地",
|
||||
"remote": "远程",
|
||||
"remoteBackupDesc": "备份数据到WebDAV",
|
||||
"remoteRecoveryDesc": "通过WebDAV恢复数据",
|
||||
"localBackupDesc": "备份数据到本地",
|
||||
"localRecoveryDesc": "通过文件恢复数据",
|
||||
"mode": "模式",
|
||||
"time": "时间",
|
||||
"source": "来源",
|
||||
@@ -340,6 +332,8 @@
|
||||
"none": "无",
|
||||
"basicConfig": "基本配置",
|
||||
"basicConfigDesc": "全局修改基本配置",
|
||||
"advancedConfig": "进阶配置",
|
||||
"advancedConfigDesc": "提供多样化配置",
|
||||
"selectedCountTitle": "已选择 {count} 项",
|
||||
"addRule": "添加规则",
|
||||
"ruleName": "规则名称",
|
||||
@@ -380,9 +374,9 @@
|
||||
"systemApp": "系统应用",
|
||||
"noNetworkApp": "无网络应用",
|
||||
"contactMe": "联系我",
|
||||
"recoveryStrategy": "恢复策略",
|
||||
"recoveryStrategy_override": "覆盖",
|
||||
"recoveryStrategy_compatible": "兼容",
|
||||
"restoreStrategy": "恢复策略",
|
||||
"restoreStrategy_override": "覆盖",
|
||||
"restoreStrategy_compatible": "兼容",
|
||||
"logsTest": "日志测试",
|
||||
"emptyTip": "{label}不能为空",
|
||||
"urlTip": "{label}必须为URL",
|
||||
@@ -435,5 +429,52 @@
|
||||
"crashlytics": "崩溃分析",
|
||||
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
|
||||
"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": "添加配置"
|
||||
}
|
||||
|
||||
|
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:
|
||||
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
||||
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
|
||||
'^lib/database/{{}}.dart': 'lib/database/generated/{{}}.g.dart'
|
||||
freezed:
|
||||
options:
|
||||
build_extensions:
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/metacubex/mihomo/constant/features"
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/hub/route"
|
||||
"github.com/metacubex/mihomo/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
@@ -36,12 +37,6 @@ var (
|
||||
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 {
|
||||
eps := make(map[string]cp.Provider)
|
||||
for n, p := range tunnel.Providers() {
|
||||
@@ -240,37 +235,19 @@ func updateConfig(params *UpdateParams) {
|
||||
updateListeners()
|
||||
}
|
||||
|
||||
func parseWithPath(path string) (*config.Config, error) {
|
||||
buf, err := readFile(path)
|
||||
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 {
|
||||
func applyConfig(params *SetupParams) error {
|
||||
runtime.GC()
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
var err error
|
||||
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 {
|
||||
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup(params.SelectedMap)
|
||||
updateListeners()
|
||||
runtime.GC()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ type ExternalProvider struct {
|
||||
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
|
||||
}
|
||||
|
||||
type ProxiesData struct {
|
||||
Proxies map[string]constant.Proxy `json:"proxies"`
|
||||
All []string `json:"all"`
|
||||
}
|
||||
|
||||
const (
|
||||
messageMethod Method = "message"
|
||||
initClashMethod Method = "initClash"
|
||||
|
||||
47
core/go.mod
@@ -10,6 +10,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
@@ -18,26 +19,22 @@ require (
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/enfein/mieru/v3 v3.20.0 // indirect
|
||||
github.com/enfein/mieru/v3 v3.26.2 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // 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-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.2 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.4.0 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/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/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
@@ -46,45 +43,50 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // 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/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/blake3 v0.1.0 // 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/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 // indirect
|
||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 // indirect
|
||||
github.com/metacubex/hkdf v0.1.0 // indirect
|
||||
github.com/metacubex/hpke v0.1.0 // indirect
|
||||
github.com/metacubex/http v0.1.0 // indirect
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 // indirect
|
||||
github.com/metacubex/mlkem v0.1.0 // indirect
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
|
||||
github.com/metacubex/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/restls-client-go v0.1.7 // indirect
|
||||
github.com/metacubex/sing v0.5.6 // 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-shadowsocks2 v0.2.7 // 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-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
|
||||
github.com/metacubex/smux v0.0.0-20250922175018-15c9a6a78719 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect
|
||||
github.com/metacubex/utls v1.8.1 // indirect
|
||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 // 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/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // 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/oschwald/maxminddb-golang v1.12.0 // 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/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/rabaead v0.0.0-20220730151906-ab6e06b96e8c // 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
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // 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
|
||||
golang.org/x/crypto v0.33.0 // 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/sys v0.30.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
|
||||
google.golang.org/protobuf v1.34.2 // 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/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
|
||||
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/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
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/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
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/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
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/enfein/mieru/v3 v3.26.2 h1:U/2XJc+3vrJD9r815FoFdwToQFEcqSOzzzWIPPhjfEU=
|
||||
github.com/enfein/mieru/v3 v3.26.2/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/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
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/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.2.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/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/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
|
||||
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
|
||||
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.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
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/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/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
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/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20250902133113-a7f637c14281 h1:09EM0sOLb2kfL0KETGhHujsBLB5iy5U/2yHRHsxf/pI=
|
||||
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 h1:vAJ0ZT4aO803F1uw2roIA9yH7Sxzox34tVVyye1bz6c=
|
||||
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/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc=
|
||||
github.com/metacubex/bart v0.24.0 h1:EyNiPeVOlg0joSHTzi5oentI0j5M89utUq/5dd76pWM=
|
||||
github.com/metacubex/bart v0.24.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bart v0.26.0 h1:d/bBTvVatfVWGfQbiDpYKI1bXUJgjaabB2KpK1Tnk6w=
|
||||
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/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/go.mod h1:CCkLdzFrqf7xmxCdhQFvJsRRV2mwOLDoSPg6vUTB9Uk=
|
||||
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/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/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ=
|
||||
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b h1:z7JLKjugnQ1qvDOAD8yMA5C8AlJY3bG+VrrgRntRlUY=
|
||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs=
|
||||
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8 h1:hUL81H0Ic/XIDkvtn9M1pmfDdfid7JzYQToY4Ps1TvQ=
|
||||
github.com/metacubex/gvisor v0.0.0-20251227095601-261ec1326fe8/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA=
|
||||
github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4=
|
||||
github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ=
|
||||
github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U=
|
||||
github.com/metacubex/http v0.1.0 h1:Jcy0I9zKjYijSUaksZU34XEe2xNdoFkgUTB7z7K5q0o=
|
||||
github.com/metacubex/http v0.1.0/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg=
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI=
|
||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc=
|
||||
github.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I=
|
||||
github.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 h1:8JVlYuE8uSJAvmyCd4TjvDxs57xjb0WxEoaWafK5+qs=
|
||||
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c=
|
||||
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
||||
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/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
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/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/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
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-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
|
||||
github.com/metacubex/sing-quic v0.0.0-20260112044712-65d17608159e h1:MLxp42z9Jd6LtY2suyawnl24oNzIsFxWc15bNeDIGxA=
|
||||
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/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
|
||||
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-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
||||
github.com/metacubex/sing-tun v0.4.8 h1:3PyiUKWXYi37yHptXskzL1723O3OUdyt0Aej4XHVikM=
|
||||
github.com/metacubex/sing-tun v0.4.8/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
||||
github.com/metacubex/sing-tun v0.4.11 h1:NG5zpvYPbBXf+9GSUmDaGCDwl3hZXV677tbRAw0QtCM=
|
||||
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/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/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-20250922175018-15c9a6a78719/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.8.1 h1:RW8GeCGWAegjV0HW5nw9DoqNoeGAXXeYUP6AysmRvx4=
|
||||
github.com/metacubex/utls v1.8.1/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 h1:DK2l6m2Fc85H2BhiAPgbJygiWhesPlfGmF+9Vw6ARdk=
|
||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141/go.mod h1:/yI4OiGOSn0SURhZdJF3CbtPg3nwK700bG8TZLMBvAg=
|
||||
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTizYE/qBHH3nEuibIelmHI+BVSxVJr8o=
|
||||
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/tls v0.1.1 h1:BEcZrsPTTfNf4sKZ02EbZodv4UIj7fgHWa1Eqo12Bc0=
|
||||
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/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
|
||||
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/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/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/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
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/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.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/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/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
||||
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/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
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-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-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-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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
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/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
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.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
|
||||
51
core/hub.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
@@ -19,11 +20,11 @@ import (
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||
"golang.org/x/exp/slices"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -43,10 +44,8 @@ func handleInitClash(paramsString string) bool {
|
||||
return false
|
||||
}
|
||||
version = params.Version
|
||||
if !isInit {
|
||||
constant.SetHomeDir(params.HomeDir)
|
||||
isInit = true
|
||||
}
|
||||
constant.SetHomeDir(params.HomeDir)
|
||||
isInit = true
|
||||
return isInit
|
||||
}
|
||||
|
||||
@@ -97,10 +96,36 @@ func handleValidateConfig(path string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleGetProxies() map[string]constant.Proxy {
|
||||
func handleGetProxies() ProxiesData {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
return tunnel.ProxiesWithProviders()
|
||||
var all []string
|
||||
all = append(all, config.ProxyList...)
|
||||
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() {
|
||||
name := proxy.Name()
|
||||
proxies[name] = proxy
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(all, "GLOBAL") {
|
||||
all = append([]string{"GLOBAL"}, all...)
|
||||
}
|
||||
|
||||
types := []constant.AdapterType{
|
||||
constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance,
|
||||
}
|
||||
nextAll := slices.DeleteFunc(all, func(name string) bool {
|
||||
return !slices.Contains(types, proxies[name].Type())
|
||||
})
|
||||
return ProxiesData{
|
||||
All: nextAll,
|
||||
Proxies: proxies,
|
||||
}
|
||||
}
|
||||
|
||||
func handleChangeProxy(data string, fn func(string string)) {
|
||||
@@ -143,7 +168,7 @@ func handleChangeProxy(data string, fn func(string string)) {
|
||||
}
|
||||
|
||||
func handleGetTraffic(onlyStatisticsProxy bool) string {
|
||||
up, down := statistic.DefaultManager.Current(onlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.NowTraffic(onlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -157,7 +182,7 @@ func handleGetTraffic(onlyStatisticsProxy bool) string {
|
||||
}
|
||||
|
||||
func handleGetTotalTraffic(onlyStatisticsProxy bool) string {
|
||||
up, down := statistic.DefaultManager.Total(onlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.TotalTraffic(onlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -287,7 +312,9 @@ func handleGetExternalProviders() string {
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -493,10 +520,10 @@ func handleSetupConfig(bytes []byte) string {
|
||||
err := UnmarshalJson(bytes, params)
|
||||
if err != nil {
|
||||
log.Errorln("unmarshalRawConfig error %v", err)
|
||||
_ = setupConfig(defaultSetupParams())
|
||||
_ = applyConfig(defaultSetupParams())
|
||||
return err.Error()
|
||||
}
|
||||
err = setupConfig(params)
|
||||
err = applyConfig(params)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
23
core/lib.go
@@ -201,9 +201,29 @@ func invokeAction(callback unsafe.Pointer, paramsChar *C.char) {
|
||||
//export startTUN
|
||||
func startTUN(callback unsafe.Pointer, fd C.int, stackChar, addressChar, dnsChar *C.char) bool {
|
||||
handleStartTun(callback, int(fd), takeCString(stackChar), takeCString(addressChar), takeCString(dnsChar))
|
||||
if !isRunning {
|
||||
handleStartListener()
|
||||
} else {
|
||||
handleResetConnections()
|
||||
}
|
||||
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
|
||||
func setEventListener(listener unsafe.Pointer) {
|
||||
if eventListener != nil || listener == nil {
|
||||
@@ -241,6 +261,9 @@ func sendMessage(message Message) {
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
handleStopTun()
|
||||
if isRunning {
|
||||
handleStopListener()
|
||||
}
|
||||
}
|
||||
|
||||
//export suspend
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -24,15 +25,15 @@ class Application extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class ApplicationState extends ConsumerState<Application> {
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
bool _preHasVpn = false;
|
||||
|
||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.android: commonSharedXPageTransitions,
|
||||
TargetPlatform.windows: commonSharedXPageTransitions,
|
||||
TargetPlatform.linux: commonSharedXPageTransitions,
|
||||
TargetPlatform.macOS: commonSharedXPageTransitions,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -46,22 +47,22 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_autoUpdateProfilesTask();
|
||||
globalState.appController = AppController(context, ref);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
globalState.appController = AppController(currentContext, ref);
|
||||
await appController.attach(currentContext, ref);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
_autoUpdateProfilesTask();
|
||||
appController.initLink();
|
||||
app?.initShortcuts();
|
||||
});
|
||||
}
|
||||
|
||||
void _autoUpdateProfilesTask() {
|
||||
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
|
||||
await globalState.appController.autoUpdateProfiles();
|
||||
await appController.autoUpdateProfiles();
|
||||
_autoUpdateProfilesTask();
|
||||
});
|
||||
}
|
||||
@@ -82,11 +83,13 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
child: CoreManager(
|
||||
child: ConnectivityManager(
|
||||
onConnectivityChanged: (results) async {
|
||||
if (!results.contains(ConnectivityResult.vpn)) {
|
||||
coreController.closeConnections();
|
||||
commonPrint.log('connectivityChanged ${results.toString()}');
|
||||
appController.updateLocalIp();
|
||||
final hasVpn = results.contains(ConnectivityResult.vpn);
|
||||
if (_preHasVpn == hasVpn) {
|
||||
appController.addCheckIp();
|
||||
}
|
||||
globalState.appController.updateLocalIp();
|
||||
globalState.appController.addCheckIpNumDebounce();
|
||||
_preHasVpn = hasVpn;
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
@@ -102,7 +105,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
}
|
||||
|
||||
Widget _buildApp({required Widget child}) {
|
||||
return MessageManager(child: ThemeManager(child: child));
|
||||
return StatusManager(child: ThemeManager(child: child));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -162,11 +165,9 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
linkManager.destroy();
|
||||
_autoUpdateGroupTaskTimer?.cancel();
|
||||
_autoUpdateProfilesTaskTimer?.cancel();
|
||||
await coreController.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
await globalState.appController.handleExit();
|
||||
await appController.handleExit();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
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 'package:archive/archive_io.dart';
|
||||
@@ -7,6 +6,9 @@ import 'package:path/path.dart';
|
||||
extension ArchiveExt on Archive {
|
||||
void addDirectoryToArchive(String dirPath, String parentPath) {
|
||||
final dir = Directory(dirPath);
|
||||
if (!dir.existsSync()) {
|
||||
return;
|
||||
}
|
||||
final entities = dir.listSync(recursive: false);
|
||||
for (final entity in entities) {
|
||||
final relativePath = relative(entity.path, from: parentPath);
|
||||
@@ -15,14 +17,11 @@ extension ArchiveExt on Archive {
|
||||
final archiveFile = ArchiveFile(relativePath, data.length, data);
|
||||
addFile(archiveFile);
|
||||
}
|
||||
// else if (entity is Directory) {
|
||||
// addDirectoryToArchive(entity.path, parentPath);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
void addTextFile<T>(String name, T raw) {
|
||||
final data = json.encode(raw);
|
||||
addFile(ArchiveFile.string(name, data));
|
||||
}
|
||||
// void addTextFile<T>(String name, T raw) {
|
||||
// final data = json.encode(raw);
|
||||
// addFile(ArchiveFile.string(name, data));
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ export 'constant.dart';
|
||||
export 'context.dart';
|
||||
export 'converter.dart';
|
||||
export 'datetime.dart';
|
||||
export 'file.dart';
|
||||
export 'fixed.dart';
|
||||
export 'function.dart';
|
||||
export 'future.dart';
|
||||
export 'hive.dart';
|
||||
export 'http.dart';
|
||||
export 'icons.dart';
|
||||
export 'indexing.dart';
|
||||
export 'iterable.dart';
|
||||
export 'keyboard.dart';
|
||||
export 'launch.dart';
|
||||
export 'link.dart';
|
||||
export 'lock.dart';
|
||||
export 'measure.dart';
|
||||
export 'migration.dart';
|
||||
export 'mixin.dart';
|
||||
export 'navigation.dart';
|
||||
export 'navigator.dart';
|
||||
@@ -32,9 +36,12 @@ export 'proxy.dart';
|
||||
export 'render.dart';
|
||||
export 'request.dart';
|
||||
export 'scroll.dart';
|
||||
export 'snowflake.dart';
|
||||
export 'string.dart';
|
||||
export 'system.dart';
|
||||
export 'task.dart';
|
||||
export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'utils.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/models/models.dart';
|
||||
|
||||
import 'string.dart';
|
||||
|
||||
List<Group> computeSort({
|
||||
required List<Group> groups,
|
||||
required ProxiesSortType sortType,
|
||||
required DelayMap delayMap,
|
||||
required SelectedMap selectedMap,
|
||||
required Map<String, String> selectedMap,
|
||||
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) {
|
||||
final proxies = group.all;
|
||||
final newProxies = switch (sortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(
|
||||
ProxiesSortType.delay => sortOfDelay(
|
||||
groups: groups,
|
||||
proxies: proxies,
|
||||
delayMap: delayMap,
|
||||
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);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
DelayState computeProxyDelayState({
|
||||
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 getRealSelectedProxyState(
|
||||
SelectedProxyState state, {
|
||||
required List<Group> groups,
|
||||
required SelectedMap selectedMap,
|
||||
required Map<String, String> selectedMap,
|
||||
}) {
|
||||
if (state.proxyName.isEmpty) return state;
|
||||
final index = groups.indexWhere((element) => element.name == state.proxyName);
|
||||
@@ -72,39 +72,39 @@ SelectedProxyState _getRealSelectedProxyState(
|
||||
if (currentSelectedName.isEmpty) {
|
||||
return newState;
|
||||
}
|
||||
return _getRealSelectedProxyState(
|
||||
return getRealSelectedProxyState(
|
||||
newState.copyWith(proxyName: currentSelectedName, testUrl: group.testUrl),
|
||||
groups: groups,
|
||||
selectedMap: selectedMap,
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfDelay({
|
||||
SelectedProxyState computeRealSelectedProxyState(
|
||||
String proxyName, {
|
||||
required List<Group> groups,
|
||||
required List<Proxy> proxies,
|
||||
required DelayMap delayMap,
|
||||
required SelectedMap selectedMap,
|
||||
required String testUrl,
|
||||
required Map<String, String> selectedMap,
|
||||
}) {
|
||||
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);
|
||||
});
|
||||
return getRealSelectedProxyState(
|
||||
SelectedProxyState(proxyName: proxyName),
|
||||
groups: groups,
|
||||
selectedMap: selectedMap,
|
||||
);
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)..sort((a, b) => a.name.compareTo(b.name));
|
||||
DelayState computeProxyDelayState({
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,14 @@ final baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16.ap,
|
||||
horizontal: 16.ap,
|
||||
);
|
||||
final listHeaderPadding = EdgeInsets.only(
|
||||
left: 16.ap,
|
||||
right: 8.ap,
|
||||
top: 24.ap,
|
||||
bottom: 8.ap,
|
||||
);
|
||||
|
||||
const watchExecution = true;
|
||||
|
||||
final defaultTextScaleFactor =
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
|
||||
@@ -57,18 +65,26 @@ final commonFilter = ImageFilter.blur(
|
||||
tileMode: TileMode.mirror,
|
||||
);
|
||||
|
||||
const listEquality = ListEquality();
|
||||
const navigationItemListEquality = ListEquality<NavigationItem>();
|
||||
const trackerInfoListEquality = ListEquality<TrackerInfo>();
|
||||
const stringListEquality = ListEquality<String>();
|
||||
const intListEquality = ListEquality<int>();
|
||||
const logListEquality = ListEquality<Log>();
|
||||
const groupListEquality = ListEquality<Group>();
|
||||
const ruleListEquality = ListEquality<Rule>();
|
||||
const scriptListEquality = ListEquality<Script>();
|
||||
const externalProviderListEquality = ListEquality<ExternalProvider>();
|
||||
const packageListEquality = ListEquality<Package>();
|
||||
const profileListEquality = ListEquality<Profile>();
|
||||
const hotKeyActionListEquality = ListEquality<HotKeyAction>();
|
||||
const stringAndStringMapEquality = MapEquality<String, String>();
|
||||
const stringAndStringMapEntryListEquality =
|
||||
ListEquality<MapEntry<String, String>>();
|
||||
const stringAndStringMapEntryIterableEquality =
|
||||
IterableEquality<MapEntry<String, String>>();
|
||||
const stringAndObjectMapEntryIterableEquality =
|
||||
IterableEquality<MapEntry<String, Object?>>();
|
||||
const delayMapEquality = MapEquality<String, Map<String, int?>>();
|
||||
const stringSetEquality = SetEquality<String>();
|
||||
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
|
||||
@@ -109,3 +125,6 @@ const scriptTemplate = '''
|
||||
const main = (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:flutter/material.dart';
|
||||
|
||||
@@ -7,8 +9,11 @@ extension BuildContextExtension on BuildContext {
|
||||
return findAncestorStateOfType<CommonScaffoldState>();
|
||||
}
|
||||
|
||||
void showNotifier(String text) {
|
||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
||||
void showNotifier(String text, {MessageActionState? actionState}) {
|
||||
return findAncestorStateOfType<StatusManagerState>()?.message(
|
||||
text,
|
||||
actionState: actionState,
|
||||
);
|
||||
}
|
||||
|
||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||
@@ -42,6 +47,8 @@ extension BuildContextExtension on BuildContext {
|
||||
|
||||
TextTheme get textTheme => Theme.of(this).textTheme;
|
||||
|
||||
AppLocalizations get appLocalizations => AppLocalizations.of(this);
|
||||
|
||||
T? findLastStateOfType<T extends State>() {
|
||||
T? state;
|
||||
|
||||
|
||||
@@ -17,23 +17,25 @@ extension DateTimeExtension on DateTime {
|
||||
final difference = currentDateTime.difference(this);
|
||||
final days = difference.inDays;
|
||||
if (days >= 365) {
|
||||
return '${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}';
|
||||
final years = (days / 365).floor();
|
||||
return appLocalizations.yearsAgo(years);
|
||||
}
|
||||
if (days >= 30) {
|
||||
return '${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}';
|
||||
final months = (days / 30).floor();
|
||||
return appLocalizations.monthsAgo(months);
|
||||
}
|
||||
if (days >= 1) {
|
||||
return '$days ${appLocalizations.days}${appLocalizations.ago}';
|
||||
return appLocalizations.daysAgo(days);
|
||||
}
|
||||
final hours = difference.inHours;
|
||||
if (hours >= 1) {
|
||||
return '$hours ${appLocalizations.hours}${appLocalizations.ago}';
|
||||
return appLocalizations.hoursAgo(hours);
|
||||
}
|
||||
final minutes = difference.inMinutes;
|
||||
if (minutes >= 1) {
|
||||
return '$minutes ${appLocalizations.minutes}${appLocalizations.ago}';
|
||||
return appLocalizations.minutesAgo(minutes);
|
||||
}
|
||||
return appLocalizations.just;
|
||||
return appLocalizations.justNow;
|
||||
}
|
||||
|
||||
String get show {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
@@ -10,19 +9,10 @@ class DAVClient {
|
||||
Completer<bool> pingCompleter = Completer();
|
||||
late String fileName;
|
||||
|
||||
DAVClient(DAV dav) {
|
||||
client = newClient(
|
||||
dav.uri,
|
||||
user: dav.user,
|
||||
password: dav.password,
|
||||
);
|
||||
DAVClient(DAVProps dav) {
|
||||
client = newClient(dav.uri, user: dav.user, password: dav.password);
|
||||
fileName = dav.fileName;
|
||||
client.setHeaders(
|
||||
{
|
||||
'accept-charset': 'utf-8',
|
||||
'Content-Type': 'text/xml',
|
||||
},
|
||||
);
|
||||
client.setHeaders({'accept-charset': 'utf-8', 'Content-Type': 'text/xml'});
|
||||
client.setConnectTimeout(8000);
|
||||
client.setSendTimeout(60000);
|
||||
client.setReceiveTimeout(60000);
|
||||
@@ -42,15 +32,16 @@ class DAVClient {
|
||||
|
||||
String get backupFile => '$root/$fileName';
|
||||
|
||||
Future<bool> backup(Uint8List data) async {
|
||||
Future<bool> backup(String localFilePath) async {
|
||||
await client.mkdir(root);
|
||||
await client.write(backupFile, data);
|
||||
await client.writeFromFile(localFilePath, backupFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<List<int>> recovery() async {
|
||||
Future<bool> restore() async {
|
||||
await client.mkdir(root);
|
||||
final data = await client.read(backupFile);
|
||||
return data;
|
||||
final backupFilePath = await appPath.backupFilePath;
|
||||
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,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) {
|
||||
final realTimout = timeout ?? const Duration(minutes: 3);
|
||||
Timer(realTimout + commonDuration, () {
|
||||
final realTimeout = timeout ?? const Duration(minutes: 3);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
}
|
||||
});
|
||||
return this.timeout(
|
||||
realTimout,
|
||||
realTimeout,
|
||||
onTimeout: () async {
|
||||
if (onTimeout != null) {
|
||||
return onTimeout();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/controller.dart';
|
||||
|
||||
class FlClashHttpOverrides extends HttpOverrides {
|
||||
static String handleFindProxy(Uri url) {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return 'DIRECT';
|
||||
}
|
||||
final port = globalState.config.patchClashConfig.mixedPort;
|
||||
final isStart = globalState.appState.runTime != null;
|
||||
final port = appController.config.patchClashConfig.mixedPort;
|
||||
final isStart = appController.isStart;
|
||||
commonPrint.log('find $url proxy:$isStart');
|
||||
if (!isStart) return 'DIRECT';
|
||||
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> {
|
||||
Iterable<T> separated(T separator) sync* {
|
||||
extension IterableExt<E> on Iterable<E> {
|
||||
Iterable<E> separated(E separator) sync* {
|
||||
final iterator = this.iterator;
|
||||
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;
|
||||
var iterator = this.iterator;
|
||||
while (iterator.moveNext()) {
|
||||
@@ -23,10 +23,7 @@ extension IterableExt<T> on Iterable<T> {
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<T> fill(
|
||||
int length, {
|
||||
required T Function(int count) filler,
|
||||
}) sync* {
|
||||
Iterable<E> fill(int length, {required E Function(int count) filler}) sync* {
|
||||
int count = 0;
|
||||
for (var item in this) {
|
||||
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();
|
||||
return count >= length ? this : toList().skip(length - count);
|
||||
}
|
||||
@@ -81,9 +78,36 @@ extension ListExt<T> on List<T> {
|
||||
return sublist(start);
|
||||
}
|
||||
|
||||
T safeGet(int index) {
|
||||
if (length > index) return this[index];
|
||||
return last;
|
||||
T? safeGet(int index, {T? defaultValue}) {
|
||||
if (index < 0 || index >= length) {
|
||||
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]!;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Measure.of(this.context, double textScaleFactor)
|
||||
: _measureMap = {},
|
||||
_textScaler = TextScaler.linear(
|
||||
textScaleFactor,
|
||||
);
|
||||
: _measureMap = {},
|
||||
_textScaler = TextScaler.linear(textScaleFactor);
|
||||
|
||||
Size computeTextSize(
|
||||
Text text, {
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
Size computeTextSize(Text text, {double maxWidth = double.infinity}) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text.data,
|
||||
style: text.style,
|
||||
),
|
||||
text: TextSpan(text: text.data, style: text.style),
|
||||
maxLines: text.maxLines,
|
||||
textScaler: _textScaler,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
)..layout(
|
||||
maxWidth: maxWidth,
|
||||
);
|
||||
)..layout(maxWidth: maxWidth);
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
@@ -35,10 +25,7 @@ class Measure {
|
||||
return _measureMap.updateCacheValue(
|
||||
'bodyMediumHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
Text('X', style: context.textTheme.bodyMedium),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
@@ -46,24 +33,16 @@ class Measure {
|
||||
double get bodyLargeHeight {
|
||||
return _measureMap.updateCacheValue(
|
||||
'bodyLargeHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
).height,
|
||||
() =>
|
||||
computeTextSize(Text('X', style: context.textTheme.bodyLarge)).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get bodySmallHeight {
|
||||
return _measureMap.updateCacheValue(
|
||||
'bodySmallHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.bodySmall,
|
||||
),
|
||||
).height,
|
||||
() =>
|
||||
computeTextSize(Text('X', style: context.textTheme.bodySmall)).height,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,10 +50,16 @@ class Measure {
|
||||
return _measureMap.updateCacheValue(
|
||||
'labelSmallHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.labelSmall,
|
||||
),
|
||||
Text('X', style: context.textTheme.labelSmall),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
|
||||
double get titleSmallHeight {
|
||||
return _measureMap.updateCacheValue(
|
||||
'titleSmallHeight',
|
||||
() => computeTextSize(
|
||||
Text('X', style: context.textTheme.titleSmall),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
@@ -83,10 +68,7 @@ class Measure {
|
||||
return _measureMap.updateCacheValue(
|
||||
'labelMediumHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.labelMedium,
|
||||
),
|
||||
Text('X', style: context.textTheme.labelMedium),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
@@ -95,10 +77,7 @@ class Measure {
|
||||
return _measureMap.updateCacheValue(
|
||||
'titleLargeHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
Text('X', style: context.textTheme.titleLarge),
|
||||
).height,
|
||||
);
|
||||
}
|
||||
@@ -107,10 +86,7 @@ class Measure {
|
||||
return _measureMap.updateCacheValue(
|
||||
'titleMediumHeight',
|
||||
() => computeTextSize(
|
||||
Text(
|
||||
'X',
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
Text('X', style: context.textTheme.titleMedium),
|
||||
).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';
|
||||
|
||||
mixin AutoDisposeNotifierMixin<T> on AnyNotifier<T, T> {
|
||||
T get value => state;
|
||||
|
||||
set value(T value) {
|
||||
if (ref.mounted) {
|
||||
state = value;
|
||||
} else {
|
||||
onUpdate(value);
|
||||
}
|
||||
state = value;
|
||||
}
|
||||
|
||||
bool equals(T previous, T next) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(previous, next) {
|
||||
final res = super.updateShouldNotify(previous, next);
|
||||
final res = !equals(previous, next)
|
||||
? super.updateShouldNotify(previous, next)
|
||||
: true;
|
||||
if (res) {
|
||||
onUpdate(next);
|
||||
}
|
||||
@@ -20,27 +24,20 @@ mixin AutoDisposeNotifierMixin<T> on AnyNotifier<T, T> {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
set value(T value) {
|
||||
if (ref.mounted) {
|
||||
state = value;
|
||||
} else {
|
||||
onUpdate(value);
|
||||
}
|
||||
state = AsyncData(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/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
static Future<T?> push<T>(BuildContext context, Widget child) async {
|
||||
if (globalState.appState.viewMode != ViewMode.mobile) {
|
||||
if (!appController.isMobile) {
|
||||
return await Navigator.of(
|
||||
context,
|
||||
).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> {
|
||||
final Widget Function(BuildContext context) builder;
|
||||
|
||||
@@ -67,14 +71,45 @@ class CommonDesktopRoute<T> extends PageRoute<T> {
|
||||
Duration get reverseTransitionDuration => Duration(milliseconds: 200);
|
||||
}
|
||||
|
||||
class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
CommonRoute({required super.builder});
|
||||
class CommonRoute<T> extends PageRoute<T> {
|
||||
final Widget Function(BuildContext context) builder;
|
||||
|
||||
CommonRoute({required this.builder});
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@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>(
|
||||
@@ -228,7 +263,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
|
||||
DecorationTween(
|
||||
begin: const _CommonEdgeShadowDecoration(),
|
||||
end: _CommonEdgeShadowDecoration(<Color>[
|
||||
widget.context.colorScheme.inverseSurface.withValues(alpha: 0.02),
|
||||
Color(0x04000000),
|
||||
Colors.transparent,
|
||||
]),
|
||||
),
|
||||
@@ -279,7 +314,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
|
||||
return;
|
||||
}
|
||||
|
||||
final double shadowWidth = 1 * configuration.size!.width;
|
||||
final double shadowWidth = 0.05 * configuration.size!.width;
|
||||
final double shadowHeight = configuration.size!.height;
|
||||
final double bandWidth = shadowWidth / (colors.length - 1);
|
||||
|
||||
|
||||
@@ -33,11 +33,25 @@ extension NumExt on num {
|
||||
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 {
|
||||
bool moreOrEqual(double value) {
|
||||
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
|
||||
return this > value || (value - this).abs() < precisionErrorTolerance + 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,19 +61,39 @@ class AppPath {
|
||||
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 {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return join(homeDirPath, 'FlClash.lock');
|
||||
}
|
||||
|
||||
Future<String> get configFilePath async {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return join(homeDirPath, 'config.json');
|
||||
final mHomeDirPath = await homeDirPath;
|
||||
return join(mHomeDirPath, 'config.yaml');
|
||||
}
|
||||
|
||||
Future<String> get validateFilePath async {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return join(homeDirPath, 'temp', 'validate${utils.id}.yaml');
|
||||
Future<String> get sharedFilePath async {
|
||||
final mHomeDirPath = await homeDirPath;
|
||||
return join(mHomeDirPath, 'shared.json');
|
||||
}
|
||||
|
||||
Future<String> get sharedPreferencesPath async {
|
||||
@@ -86,9 +106,18 @@ class AppPath {
|
||||
return join(directory.path, profilesDirectoryName);
|
||||
}
|
||||
|
||||
Future<String> getProfilePath(String id) async {
|
||||
final directory = await profilesPath;
|
||||
return join(directory, '$id.yaml');
|
||||
Future<String> getProfilePath(String fileName) async {
|
||||
return join(await profilesPath, '$fileName.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 {
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class Picker {
|
||||
Future<PlatformFile?> pickerFile() async {
|
||||
Future<PlatformFile?> pickerFile({bool withData = true}) async {
|
||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||
withData: true,
|
||||
withData: withData,
|
||||
allowMultiple: false,
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
);
|
||||
@@ -20,15 +20,33 @@ class Picker {
|
||||
final path = await FilePicker.platform.saveFile(
|
||||
fileName: fileName,
|
||||
initialDirectory: await appPath.downloadDirPath,
|
||||
bytes: system.isAndroid ? bytes : null,
|
||||
bytes: bytes,
|
||||
);
|
||||
if (!system.isAndroid && path != null) {
|
||||
final file = await File(path).create(recursive: true);
|
||||
await file.writeAsBytes(bytes);
|
||||
final file = File(path);
|
||||
await file.safeWriteAsBytes(bytes);
|
||||
}
|
||||
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 {
|
||||
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||
if (xFile == null) {
|
||||
|
||||
@@ -24,36 +24,70 @@ class Preferences {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<ClashConfig?> getClashConfig() async {
|
||||
Future<int> getVersion() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||
if (clashConfigString == null) return null;
|
||||
final clashConfigMap = json.decode(clashConfigString);
|
||||
return ClashConfig.fromJson(clashConfigMap);
|
||||
return preferences?.getInt('version') ?? 0;
|
||||
}
|
||||
|
||||
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 {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final configMap = json.decode(configString);
|
||||
return Config.compatibleFromJson(configMap);
|
||||
final configMap = await getConfigMap();
|
||||
if (configMap == null) {
|
||||
return null;
|
||||
}
|
||||
return Config.fromJson(configMap);
|
||||
}
|
||||
|
||||
Future<bool> saveConfig(Config config) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return await preferences?.setString(configKey, json.encode(config)) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<void> clearClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
preferences?.remove(clashConfigKey);
|
||||
return preferences?.setString(configKey, json.encode(config)) ?? false;
|
||||
}
|
||||
|
||||
Future<void> clearPreferences() async {
|
||||
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/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonPrint {
|
||||
static CommonPrint? _instance;
|
||||
@@ -16,12 +16,10 @@ class CommonPrint {
|
||||
void log(String? text, {LogLevel logLevel = LogLevel.info}) {
|
||||
final payload = '[APP] $text';
|
||||
debugPrint(payload);
|
||||
if (!globalState.isInit) {
|
||||
if (!appController.isAttach) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.addLog(
|
||||
Log.app(payload).copyWith(logLevel: logLevel),
|
||||
);
|
||||
appController.addLog(Log.app(payload).copyWith(logLevel: logLevel));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.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/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -22,7 +24,7 @@ class Request {
|
||||
createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
client.findProxy = (Uri uri) {
|
||||
client.userAgent = globalState.ua;
|
||||
client.userAgent = appController.ua;
|
||||
return FlClashHttpOverrides.handleFindProxy(uri);
|
||||
};
|
||||
return client;
|
||||
@@ -30,16 +32,28 @@ class Request {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> getFileResponseForUrl(String url) async {
|
||||
final response = await _clashDio.get(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
return response;
|
||||
Future<Response<Uint8List>> getFileResponseForUrl(String url) async {
|
||||
try {
|
||||
return await _clashDio.get<Uint8List>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
} 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 {
|
||||
final response = await _clashDio.get(
|
||||
Future<Response<String>> getTextResponseForUrl(String url) async {
|
||||
final response = await _clashDio.get<String>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.plain),
|
||||
);
|
||||
@@ -58,18 +72,23 @@ class Request {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> checkForUpdate() async {
|
||||
final response = await dio.get(
|
||||
'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>;
|
||||
final remoteVersion = data['tag_name'];
|
||||
final version = globalState.packageInfo.version;
|
||||
final hasUpdate =
|
||||
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
if (!hasUpdate) return null;
|
||||
return data;
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'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>;
|
||||
final remoteVersion = data['tag_name'];
|
||||
final version = globalState.packageInfo.version;
|
||||
final hasUpdate =
|
||||
utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
|
||||
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 = {
|
||||
@@ -84,6 +103,7 @@ class Request {
|
||||
|
||||
Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
|
||||
var failureCount = 0;
|
||||
final token = cancelToken ?? CancelToken();
|
||||
final futures = _ipInfoSources.entries.map((source) async {
|
||||
final Completer<Result<IpInfo?>> completer = Completer();
|
||||
handleFailRes() {
|
||||
@@ -95,7 +115,7 @@ class Request {
|
||||
final future = dio
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
cancelToken: token,
|
||||
options: Options(responseType: ResponseType.json),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -118,7 +138,7 @@ class Request {
|
||||
return completer.future;
|
||||
});
|
||||
final res = await Future.any(futures);
|
||||
cancelToken?.cancel();
|
||||
token.cancel();
|
||||
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 'package:crypto/crypto.dart';
|
||||
|
||||
import 'print.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
bool get isUrl {
|
||||
@@ -22,6 +21,16 @@ extension StringExtension on String {
|
||||
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 {
|
||||
final byteData = ByteData(length * 2);
|
||||
final bom = [0xFF, 0xFE];
|
||||
@@ -68,13 +77,26 @@ extension StringExtension on String {
|
||||
// bool containsToLower(String target) {
|
||||
// return toLowerCase().contains(target);
|
||||
// }
|
||||
}
|
||||
|
||||
extension StringExtensionSafe on String? {
|
||||
String getSafeValue(String defaultValue) {
|
||||
if (this == null || this!.isEmpty) {
|
||||
return defaultValue;
|
||||
Future<T> commonToJSON<T>() async {
|
||||
final thresholdLimit = 51200;
|
||||
if (length < thresholdLimit) {
|
||||
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));
|
||||
final retryStatus = await retry(
|
||||
task: checkService,
|
||||
retryIf: (status) => status == WindowsHelperServiceStatus.running,
|
||||
delay: commonDuration,
|
||||
maxAttempts: 5,
|
||||
retryIf: (status) => status != WindowsHelperServiceStatus.running,
|
||||
delay: Duration(seconds: 1),
|
||||
);
|
||||
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 'package:fl_clash/common/utils.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/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
@@ -15,38 +13,56 @@ import 'system.dart';
|
||||
import 'window.dart';
|
||||
|
||||
class Tray {
|
||||
static Tray? _instance;
|
||||
|
||||
Tray._internal();
|
||||
|
||||
factory Tray() {
|
||||
_instance ??= Tray._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
String get trayIconSuffix {
|
||||
return system.isWindows ? 'ico' : 'png';
|
||||
}
|
||||
|
||||
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({
|
||||
required Brightness? brightness,
|
||||
bool force = false,
|
||||
required bool isStart,
|
||||
required bool tunEnable,
|
||||
}) async {
|
||||
if (Platform.isLinux || force) {
|
||||
if (Platform.isLinux) {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
await trayManager.setIcon(
|
||||
utils.getTrayIconPath(
|
||||
brightness: brightness ??
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
),
|
||||
getTryIcon(isStart: isStart, tunEnable: tunEnable),
|
||||
isTemplate: true,
|
||||
);
|
||||
if (!Platform.isLinux) {
|
||||
await trayManager.setToolTip(
|
||||
appName,
|
||||
);
|
||||
await trayManager.setToolTip(appName);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> update({
|
||||
required TrayState trayState,
|
||||
bool focus = false,
|
||||
required Traffic traffic,
|
||||
}) async {
|
||||
if (system.isAndroid) {
|
||||
return;
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
if (!system.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
isStart: trayState.isStart,
|
||||
tunEnable: trayState.tunEnable,
|
||||
);
|
||||
}
|
||||
List<MenuItem> menuItems = [];
|
||||
@@ -60,18 +76,28 @@ class Tray {
|
||||
final startMenuItem = MenuItem.checkbox(
|
||||
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateStart();
|
||||
appController.updateStart();
|
||||
},
|
||||
checked: false,
|
||||
);
|
||||
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());
|
||||
for (final mode in Mode.values) {
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: Intl.message(mode.name),
|
||||
onClick: (_) {
|
||||
globalState.appController.changeMode(mode);
|
||||
appController.changeMode(mode);
|
||||
},
|
||||
checked: mode == trayState.mode,
|
||||
),
|
||||
@@ -85,13 +111,10 @@ class Tray {
|
||||
subMenuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: proxy.name,
|
||||
checked: trayState.selectedMap[group.name] == proxy.name,
|
||||
checked:
|
||||
appController.getSelectedProxyName(group.name) == proxy.name,
|
||||
onClick: (_) {
|
||||
final appController = globalState.appController;
|
||||
appController.updateCurrentSelectedMap(
|
||||
group.name,
|
||||
proxy.name,
|
||||
);
|
||||
appController.updateCurrentSelectedMap(group.name, proxy.name);
|
||||
appController.changeProxy(
|
||||
groupName: group.name,
|
||||
proxyName: proxy.name,
|
||||
@@ -103,9 +126,7 @@ class Tray {
|
||||
menuItems.add(
|
||||
MenuItem.submenu(
|
||||
label: group.name,
|
||||
submenu: Menu(
|
||||
items: subMenuItems,
|
||||
),
|
||||
submenu: Menu(items: subMenuItems),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -118,7 +139,7 @@ class Tray {
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.tun,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateTun();
|
||||
appController.updateTun();
|
||||
},
|
||||
checked: trayState.tunEnable,
|
||||
),
|
||||
@@ -127,7 +148,7 @@ class Tray {
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.systemProxy,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateSystemProxy();
|
||||
appController.updateSystemProxy();
|
||||
},
|
||||
checked: trayState.systemProxy,
|
||||
),
|
||||
@@ -137,7 +158,7 @@ class Tray {
|
||||
final autoStartMenuItem = MenuItem.checkbox(
|
||||
label: appLocalizations.autoLaunch,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateAutoLaunch();
|
||||
appController.updateAutoLaunch();
|
||||
},
|
||||
checked: trayState.autoLaunch,
|
||||
);
|
||||
@@ -153,44 +174,43 @@ class Tray {
|
||||
final exitMenuItem = MenuItem(
|
||||
label: appLocalizations.exit,
|
||||
onClick: (_) async {
|
||||
await globalState.appController.handleExit();
|
||||
await appController.handleExit();
|
||||
},
|
||||
);
|
||||
menuItems.add(exitMenuItem);
|
||||
final menu = Menu(items: menuItems);
|
||||
await trayManager.setContextMenu(menu);
|
||||
if (Platform.isLinux) {
|
||||
if (system.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
isStart: trayState.isStart,
|
||||
tunEnable: trayState.tunEnable,
|
||||
);
|
||||
}
|
||||
updateTrayTitle(showTrayTitle: trayState.showTrayTitle, traffic: traffic);
|
||||
}
|
||||
|
||||
Future<void> updateTrayTitle([Traffic? traffic]) async {
|
||||
// if (!system.isMacOS) {
|
||||
// return;
|
||||
// }
|
||||
// if (traffic == null) {
|
||||
// await trayManager.setTitle("");
|
||||
// } else {
|
||||
// await trayManager.setTitle(
|
||||
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
|
||||
// );
|
||||
// }
|
||||
Future<void> updateTrayTitle({
|
||||
required bool showTrayTitle,
|
||||
required Traffic traffic,
|
||||
}) async {
|
||||
if (!system.isMacOS) {
|
||||
return;
|
||||
}
|
||||
if (!showTrayTitle) {
|
||||
await trayManager.setTitle('');
|
||||
} else {
|
||||
await trayManager.setTitle(traffic.trayTitle);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _copyEnv(int port) async {
|
||||
final url = 'http://127.0.0.1:$port';
|
||||
|
||||
final cmdline =
|
||||
system.isWindows ? 'set \$env:all_proxy=$url' : 'export all_proxy=$url';
|
||||
final cmdline = system.isWindows
|
||||
? 'set \$env:all_proxy=$url'
|
||||
: 'export all_proxy=$url';
|
||||
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: cmdline,
|
||||
),
|
||||
);
|
||||
await Clipboard.setData(ClipboardData(text: cmdline));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Utils {
|
||||
static Utils? _instance;
|
||||
|
||||
Utils._internal();
|
||||
|
||||
factory Utils() {
|
||||
_instance ??= Utils._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Color? getDelayColor(int? delay) {
|
||||
if (delay == null) return null;
|
||||
if (delay < 0) return Colors.red;
|
||||
@@ -142,16 +151,9 @@ class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
String getTrayIconPath({required Brightness brightness}) {
|
||||
if (system.isMacOS) {
|
||||
return 'assets/images/icon_white.png';
|
||||
}
|
||||
String get traySuffix {
|
||||
final suffix = system.isWindows ? 'ico' : 'png';
|
||||
return 'assets/images/icon.$suffix';
|
||||
// return switch (brightness) {
|
||||
// Brightness.dark => "assets/images/icon_white.$suffix",
|
||||
// Brightness.light => "assets/images/icon_black.$suffix",
|
||||
// };
|
||||
return 'assets/images/icon/status_2.$suffix';
|
||||
}
|
||||
|
||||
int compareVersions(String version1, String version2) {
|
||||
@@ -326,7 +328,7 @@ class Utils {
|
||||
required Function function,
|
||||
required void Function(T data, int elapsedMilliseconds) onWatch,
|
||||
}) async {
|
||||
if (kDebugMode) {
|
||||
if (kDebugMode && watchExecution) {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final res = await function();
|
||||
stopwatch.stop();
|
||||
@@ -335,6 +337,21 @@ class Utils {
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
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:screen_retriever/screen_retriever.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class Window {
|
||||
Future<void> init(int version) async {
|
||||
final props = globalState.config.windowProps;
|
||||
static Window? _instance;
|
||||
|
||||
Window._internal();
|
||||
|
||||
factory Window() {
|
||||
_instance ??= Window._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> init(int version, WindowProps props) async {
|
||||
final acquire = await singleInstanceLock.acquire();
|
||||
if (!acquire) {
|
||||
exit(0);
|
||||
@@ -19,13 +27,22 @@ class Window {
|
||||
protocol.register('flclash');
|
||||
}
|
||||
await windowManager.ensureInitialized();
|
||||
// kDebugMode ? Size(680, 580) :
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
size: props.size,
|
||||
minimumSize: const Size(380, 400),
|
||||
);
|
||||
if (!system.isMacOS || version > 10) {
|
||||
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) {
|
||||
final left = props.left ?? 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 {
|
||||
|
||||
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();
|
||||
1772
lib/controller.dart
@@ -1,14 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/core/core.dart';
|
||||
import 'package:fl_clash/core/interface.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
@@ -73,19 +71,17 @@ class CoreController {
|
||||
|
||||
FutureOr<bool> get isInit => _interface.isInit;
|
||||
|
||||
Future<String> validateConfig(String data) async {
|
||||
final path = await appPath.validateFilePath;
|
||||
await globalState.genValidateFile(path, data);
|
||||
Future<String> validateConfig(String path) async {
|
||||
final res = await _interface.validateConfig(path);
|
||||
await File(path).delete();
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<String> validateConfigFormBytes(Uint8List bytes) async {
|
||||
final path = await appPath.validateFilePath;
|
||||
await globalState.genValidateFileFormBytes(path, bytes);
|
||||
Future<String> validateConfigWithData(String data) async {
|
||||
final path = await appPath.tempFilePath;
|
||||
final file = File(path);
|
||||
await file.safeWriteAsString(data);
|
||||
final res = await _interface.validateConfig(path);
|
||||
await File(path).delete();
|
||||
await File(path).safeDelete();
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -93,12 +89,11 @@ class CoreController {
|
||||
return await _interface.updateConfig(updateParams);
|
||||
}
|
||||
|
||||
Future<String> setupConfig(
|
||||
ClashConfig clashConfig, {
|
||||
Future<String> setupConfig({
|
||||
required SetupParams params,
|
||||
required SetupState setupState,
|
||||
VoidCallback? preloadInvoke,
|
||||
}) async {
|
||||
await globalState.genConfigFile(clashConfig);
|
||||
final params = await globalState.getSetupParams();
|
||||
final res = _interface.setupConfig(params);
|
||||
if (preloadInvoke != null) {
|
||||
preloadInvoke();
|
||||
@@ -109,36 +104,19 @@ class CoreController {
|
||||
Future<List<Group>> getProxiesGroups({
|
||||
required ProxiesSortType sortType,
|
||||
required DelayMap delayMap,
|
||||
required SelectedMap selectedMap,
|
||||
required Map<String, String> selectedMap,
|
||||
required String defaultTestUrl,
|
||||
}) async {
|
||||
final proxies = await _interface.getProxies();
|
||||
return Isolate.run<List<Group>>(() {
|
||||
if (proxies.isEmpty) return [];
|
||||
final groupNames = [
|
||||
UsedProxy.GLOBAL.name,
|
||||
...(proxies[UsedProxy.GLOBAL.name]['all'] as List).where((e) {
|
||||
final proxy = proxies[e] ?? {};
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']);
|
||||
}),
|
||||
];
|
||||
final groupsRaw = groupNames.map((groupName) {
|
||||
final group = proxies[groupName];
|
||||
group['all'] = ((group['all'] ?? []) as List)
|
||||
.map((name) => proxies[name])
|
||||
.where((proxy) => proxy != null)
|
||||
.toList();
|
||||
return group;
|
||||
}).toList();
|
||||
final groups = groupsRaw.map((e) => Group.fromJson(e)).toList();
|
||||
return computeSort(
|
||||
groups: groups,
|
||||
final proxiesData = await _interface.getProxies();
|
||||
return toGroupsTask(
|
||||
ComputeGroupsState(
|
||||
proxiesData: proxiesData,
|
||||
sortType: sortType,
|
||||
delayMap: delayMap,
|
||||
selectedMap: selectedMap,
|
||||
defaultTestUrl: defaultTestUrl,
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
|
||||
@@ -169,13 +147,11 @@ class CoreController {
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return Isolate.run<List<ExternalProvider>>(() {
|
||||
final externalProviders =
|
||||
(json.decode(externalProvidersRawString) as List<dynamic>)
|
||||
.map((item) => ExternalProvider.fromJson(item))
|
||||
.toList();
|
||||
return externalProviders;
|
||||
});
|
||||
final externalProviders =
|
||||
(await externalProvidersRawString.commonToJSON<List<dynamic>>())
|
||||
.map((item) => ExternalProvider.fromJson(item))
|
||||
.toList();
|
||||
return externalProviders;
|
||||
}
|
||||
|
||||
Future<ExternalProvider?> getExternalProvider(
|
||||
@@ -221,10 +197,12 @@ class CoreController {
|
||||
return Delay.fromJson(json.decode(data));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getConfig(String id) async {
|
||||
final profilePath = await appPath.getProfilePath(id);
|
||||
Future<Map<String, dynamic>> getConfig(int id) async {
|
||||
final profilePath = await appPath.getProfilePath(id.toString());
|
||||
final res = await _interface.getConfig(profilePath);
|
||||
if (res.isSuccess) {
|
||||
res.data['rules'] = res.data['rule'];
|
||||
res.data.remove('rule');
|
||||
return res.data;
|
||||
} else {
|
||||
throw res.message;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
@@ -28,7 +27,7 @@ mixin CoreInterface {
|
||||
|
||||
Future<String> setupConfig(SetupParams setupParams);
|
||||
|
||||
Future<Map> getProxies();
|
||||
Future<ProxiesData> getProxies();
|
||||
|
||||
Future<String> changeProxy(ChangeProxyParams changeProxyParams);
|
||||
|
||||
@@ -86,8 +85,16 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
await completer.future;
|
||||
if (kDebugMode) {
|
||||
try {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -162,10 +169,9 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
|
||||
@override
|
||||
Future<String> setupConfig(SetupParams setupParams) async {
|
||||
final data = await Isolate.run(() => json.encode(setupParams));
|
||||
return await _invoke<String>(
|
||||
method: ActionMethod.setupConfig,
|
||||
data: data,
|
||||
data: json.encode(setupParams),
|
||||
) ??
|
||||
'';
|
||||
}
|
||||
@@ -176,9 +182,13 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map> getProxies() async {
|
||||
final map = await _invoke<Map>(method: ActionMethod.getProxies);
|
||||
return map ?? {};
|
||||
Future<ProxiesData> getProxies() async {
|
||||
final data = await _invoke<Map<String, dynamic>>(
|
||||
method: ActionMethod.getProxies,
|
||||
);
|
||||
return data != null
|
||||
? ProxiesData.fromJson(data)
|
||||
: ProxiesData(proxies: {}, all: []);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/core.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'interface.dart';
|
||||
|
||||
@@ -22,9 +22,7 @@ class CoreLib extends CoreHandlerInterface {
|
||||
return res ?? '';
|
||||
}
|
||||
_connectedCompleter.complete(true);
|
||||
final syncRes = await service?.syncAndroidState(
|
||||
globalState.getAndroidState(),
|
||||
);
|
||||
final syncRes = await service?.syncState(appController.sharedState);
|
||||
return syncRes ?? '';
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,9 @@ class CoreService extends CoreHandlerInterface {
|
||||
.transform(uint8ListToListIntConverter)
|
||||
.transform(utf8.decoder)
|
||||
.transform(LineSplitter())
|
||||
.listen((data) {
|
||||
handleResult(ActionResult.fromJson(json.decode(data.trim())));
|
||||
.listen((data) async {
|
||||
final dataJson = await data.trim().commonToJSON<dynamic>();
|
||||
handleResult(ActionResult.fromJson(dataJson));
|
||||
})
|
||||
.onDone(() {
|
||||
_handleInvokeCrashEvent();
|
||||
@@ -112,6 +113,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await _serverCompleter.future;
|
||||
await shutdown();
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
@@ -125,9 +127,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
Future<void> _deleteSocketFile() async {
|
||||
if (!system.isWindows) {
|
||||
final file = File(unixSocketPath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
await file.safeDelete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
lib/database/profiles.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
part of 'database.dart';
|
||||
|
||||
@DataClassName('RawProfile')
|
||||
class Profiles extends Table {
|
||||
@override
|
||||
String get tableName => 'profiles';
|
||||
|
||||
IntColumn get id => integer()();
|
||||
|
||||
TextColumn get label => text()();
|
||||
|
||||
TextColumn get currentGroupName => text().nullable()();
|
||||
|
||||
TextColumn get url => text()();
|
||||
|
||||
DateTimeColumn get lastUpdateDate => dateTime().nullable()();
|
||||
|
||||
TextColumn get overwriteType => textEnum<OverwriteType>()();
|
||||
|
||||
IntColumn get scriptId => integer().nullable()();
|
||||
|
||||
IntColumn get autoUpdateDurationMillis => integer()();
|
||||
|
||||
TextColumn get subscriptionInfo =>
|
||||
text().map(const SubscriptionInfoConverter()).nullable()();
|
||||
|
||||
BoolColumn get autoUpdate => boolean()();
|
||||
|
||||
TextColumn get selectedMap => text().map(const StringMapConverter())();
|
||||
|
||||
TextColumn get unfoldSet => text().map(const StringSetConverter())();
|
||||
|
||||
IntColumn get order => integer().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class SubscriptionInfoConverter
|
||||
extends TypeConverter<SubscriptionInfo?, String?> {
|
||||
const SubscriptionInfoConverter();
|
||||
|
||||
@override
|
||||
SubscriptionInfo? fromSql(String? fromDb) {
|
||||
if (fromDb == null) return null;
|
||||
return SubscriptionInfo.fromJson(json.decode(fromDb));
|
||||
}
|
||||
|
||||
@override
|
||||
String? toSql(SubscriptionInfo? value) {
|
||||
if (value == null) return null;
|
||||
return json.encode(value.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@DriftAccessor(tables: [Profiles])
|
||||
class ProfilesDao extends DatabaseAccessor<Database> with _$ProfilesDaoMixin {
|
||||
ProfilesDao(super.attachedDatabase);
|
||||
|
||||
Selectable<Profile> all() {
|
||||
final stmt = profiles.select();
|
||||
stmt.orderBy([
|
||||
(t) => OrderingTerm(expression: t.order, nulls: NullsOrder.last),
|
||||
(t) => OrderingTerm.asc(t.id),
|
||||
]);
|
||||
return stmt.map((item) => item.toProfile());
|
||||
}
|
||||
|
||||
Future<void> setAll(Iterable<Profile> profiles) async {
|
||||
await batch((b) async {
|
||||
setAllWithBatch(b, profiles);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> putAll<T extends Table, D extends DataClass>(
|
||||
Iterable<Insertable<D>> items,
|
||||
) async {
|
||||
await batch((b) async {
|
||||
putAllWithBatch(b, items);
|
||||
});
|
||||
}
|
||||
|
||||
void putAllWithBatch<T extends Table, D extends DataClass>(
|
||||
Batch batch,
|
||||
Iterable<Insertable<D>> items,
|
||||
) {
|
||||
batch.insertAllOnConflictUpdate(profiles, items);
|
||||
}
|
||||
|
||||
void setAllWithBatch(Batch batch, Iterable<Profile> profiles) {
|
||||
final List<ProfilesCompanion> items = [];
|
||||
final List<int> ids = [];
|
||||
profiles.forEachIndexed((index, profile) {
|
||||
ids.add(profile.id);
|
||||
items.add(profile.toCompanion(index));
|
||||
});
|
||||
|
||||
this.profiles.setAll(batch, items, deleteFilter: (t) => t.id.isNotIn(ids));
|
||||
}
|
||||
}
|
||||
|
||||
class StringMapConverter extends TypeConverter<Map<String, String>, String> {
|
||||
const StringMapConverter();
|
||||
|
||||
@override
|
||||
Map<String, String> fromSql(String fromDb) {
|
||||
return Map<String, String>.from(json.decode(fromDb));
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(Map<String, String> value) {
|
||||
return json.encode(value);
|
||||
}
|
||||
}
|
||||
|
||||
class StringSetConverter extends TypeConverter<Set<String>, String> {
|
||||
const StringSetConverter();
|
||||
|
||||
@override
|
||||
Set<String> fromSql(String fromDb) {
|
||||
return Set<String>.from(json.decode(fromDb));
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(Set<String> value) {
|
||||
return json.encode(value.toList());
|
||||
}
|
||||
}
|
||||
|
||||
extension RawProfilExt on RawProfile {
|
||||
Profile toProfile() {
|
||||
return Profile(
|
||||
id: id,
|
||||
label: label,
|
||||
currentGroupName: currentGroupName,
|
||||
url: url,
|
||||
lastUpdateDate: lastUpdateDate,
|
||||
autoUpdateDuration: Duration(milliseconds: autoUpdateDurationMillis),
|
||||
subscriptionInfo: subscriptionInfo,
|
||||
autoUpdate: autoUpdate,
|
||||
selectedMap: selectedMap,
|
||||
unfoldSet: unfoldSet,
|
||||
overwriteType: overwriteType,
|
||||
scriptId: scriptId,
|
||||
order: order,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfilesCompanionExt on Profile {
|
||||
ProfilesCompanion toCompanion([int? order]) {
|
||||
return ProfilesCompanion.insert(
|
||||
id: Value(id),
|
||||
label: label,
|
||||
currentGroupName: Value(currentGroupName),
|
||||
url: url,
|
||||
lastUpdateDate: Value(lastUpdateDate),
|
||||
autoUpdateDurationMillis: autoUpdateDuration.inMilliseconds,
|
||||
subscriptionInfo: Value(subscriptionInfo),
|
||||
autoUpdate: autoUpdate,
|
||||
selectedMap: selectedMap,
|
||||
unfoldSet: unfoldSet,
|
||||
overwriteType: overwriteType,
|
||||
scriptId: Value(scriptId),
|
||||
order: Value(order ?? this.order),
|
||||
);
|
||||
}
|
||||
}
|
||||
283
lib/database/rules.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
part of 'database.dart';
|
||||
|
||||
@DataClassName('RawRule')
|
||||
class Rules extends Table {
|
||||
@override
|
||||
String get tableName => 'rules';
|
||||
|
||||
IntColumn get id => integer()();
|
||||
|
||||
TextColumn get value => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DriftAccessor(tables: [Rules, ProfileRuleLinks])
|
||||
class RulesDao extends DatabaseAccessor<Database> with _$RulesDaoMixin {
|
||||
RulesDao(super.attachedDatabase);
|
||||
|
||||
Selectable<Rule> allGlobalAddedRules() {
|
||||
return _get();
|
||||
}
|
||||
|
||||
Selectable<Rule> allProfileAddedRules(int profileId) {
|
||||
return _get(profileId: profileId, scene: RuleScene.added);
|
||||
}
|
||||
|
||||
Selectable<Rule> allProfileDisabledRules(int profileId) {
|
||||
return _get(profileId: profileId, scene: RuleScene.disabled);
|
||||
}
|
||||
|
||||
Selectable<Rule> allAddedRules(int profileId) {
|
||||
final disabledIdsQuery = selectOnly(profileRuleLinks)
|
||||
..addColumns([profileRuleLinks.ruleId])
|
||||
..where(
|
||||
profileRuleLinks.profileId.equals(profileId) &
|
||||
profileRuleLinks.scene.equalsValue(RuleScene.disabled),
|
||||
);
|
||||
|
||||
final query = select(rules).join([
|
||||
innerJoin(profileRuleLinks, profileRuleLinks.ruleId.equalsExp(rules.id)),
|
||||
]);
|
||||
|
||||
query.where(
|
||||
(profileRuleLinks.profileId.isNull() |
|
||||
(profileRuleLinks.profileId.equals(profileId) &
|
||||
profileRuleLinks.scene.equalsValue(RuleScene.added))) &
|
||||
profileRuleLinks.ruleId.isNotInQuery(disabledIdsQuery),
|
||||
);
|
||||
|
||||
query.orderBy([
|
||||
OrderingTerm.desc(
|
||||
profileRuleLinks.profileId.isNull().caseMatch<int>(
|
||||
when: {const Constant(true): const Constant(1)},
|
||||
orElse: const Constant(0),
|
||||
),
|
||||
),
|
||||
OrderingTerm.desc(profileRuleLinks.order),
|
||||
]);
|
||||
|
||||
return query.map((row) {
|
||||
final ruleData = row.readTable(rules);
|
||||
final order = row.read(profileRuleLinks.order);
|
||||
return ruleData.toRule(order);
|
||||
});
|
||||
}
|
||||
|
||||
void restoreWithBatch(
|
||||
Batch batch,
|
||||
Iterable<Rule> rules,
|
||||
Iterable<ProfileRuleLink> links,
|
||||
) {
|
||||
batch.insertAllOnConflictUpdate(
|
||||
this.rules,
|
||||
rules.map((item) => item.toCompanion()),
|
||||
);
|
||||
final ruleIds = rules.map((item) => item.id);
|
||||
batch.deleteWhere(this.rules, (t) => t.id.isNotIn(ruleIds));
|
||||
batch.insertAllOnConflictUpdate(
|
||||
profileRuleLinks,
|
||||
links.map((item) => item.toCompanion()),
|
||||
);
|
||||
final linkKeys = links.map((item) => item.key);
|
||||
batch.deleteWhere(profileRuleLinks, (t) => t.id.isNotIn(linkKeys));
|
||||
}
|
||||
|
||||
Future<void> delRules(Iterable<int> ruleIds) {
|
||||
return _delAll(ruleIds);
|
||||
}
|
||||
|
||||
Future<void> putGlobalRule(Rule rule) {
|
||||
return _put(rule);
|
||||
}
|
||||
|
||||
Future<void> putProfileAddedRule(int profileId, Rule rule) {
|
||||
return _put(rule, profileId: profileId, scene: RuleScene.added);
|
||||
}
|
||||
|
||||
Future<void> putProfileDisabledRule(int profileId, Rule rule) {
|
||||
return _put(rule, profileId: profileId, scene: RuleScene.added);
|
||||
}
|
||||
|
||||
Future<void> putGlobalRules(Iterable<Rule> rules) {
|
||||
return _putAll(rules);
|
||||
}
|
||||
|
||||
Future<void> setGlobalRules(Iterable<Rule> rules) {
|
||||
return _set(rules);
|
||||
}
|
||||
|
||||
Future<int> putDisabledLink(int profileId, int ruleId) async {
|
||||
return await profileRuleLinks.insertOnConflictUpdate(
|
||||
ProfileRuleLink(
|
||||
ruleId: ruleId,
|
||||
profileId: profileId,
|
||||
scene: RuleScene.disabled,
|
||||
).toCompanion(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> delDisabledLink(int profileId, int ruleId) async {
|
||||
return await profileRuleLinks.deleteOne(
|
||||
ProfileRuleLink(
|
||||
profileId: profileId,
|
||||
ruleId: ruleId,
|
||||
scene: RuleScene.disabled,
|
||||
).toCompanion(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> orderGlobalRule({
|
||||
required int ruleId,
|
||||
required String order,
|
||||
}) async {
|
||||
return await _order(ruleId: ruleId, order: order);
|
||||
}
|
||||
|
||||
Future<int> orderProfileAddedRule(
|
||||
int profileId, {
|
||||
required int ruleId,
|
||||
required String order,
|
||||
}) async {
|
||||
return await _order(
|
||||
ruleId: ruleId,
|
||||
order: order,
|
||||
profileId: profileId,
|
||||
scene: RuleScene.added,
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<Rule> _get({int? profileId, RuleScene? scene}) {
|
||||
final query = select(rules).join([
|
||||
innerJoin(profileRuleLinks, profileRuleLinks.ruleId.equalsExp(rules.id)),
|
||||
]);
|
||||
|
||||
query.where(
|
||||
profileId == null
|
||||
? profileRuleLinks.profileId.isNull()
|
||||
: profileRuleLinks.profileId.equals(profileId) &
|
||||
profileRuleLinks.scene.equalsValue(scene),
|
||||
);
|
||||
|
||||
query.orderBy([
|
||||
OrderingTerm.desc(profileRuleLinks.order),
|
||||
OrderingTerm.desc(profileRuleLinks.id),
|
||||
]);
|
||||
|
||||
return query.map((row) {
|
||||
return row.readTable(rules).toRule(row.read(profileRuleLinks.order));
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> _order({
|
||||
required int ruleId,
|
||||
required String order,
|
||||
int? profileId,
|
||||
RuleScene? scene,
|
||||
}) async {
|
||||
final stmt = profileRuleLinks.update();
|
||||
stmt.where((t) {
|
||||
return (profileId == null
|
||||
? t.profileId.isNull()
|
||||
: t.profileId.equals(profileId)) &
|
||||
t.ruleId.equals(ruleId) &
|
||||
t.scene.equalsValue(scene);
|
||||
});
|
||||
return await stmt.write(ProfileRuleLinksCompanion(order: Value(order)));
|
||||
}
|
||||
|
||||
Future<int> _put(Rule rule, {int? profileId, RuleScene? scene}) async {
|
||||
return transaction(() async {
|
||||
final row = await rules.insertOnConflictUpdate(rule.toCompanion());
|
||||
if (row == 0) {
|
||||
return 0;
|
||||
}
|
||||
return await profileRuleLinks.insertOnConflictUpdate(
|
||||
ProfileRuleLink(
|
||||
ruleId: rule.id,
|
||||
profileId: profileId,
|
||||
scene: scene,
|
||||
).toCompanion(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _delAll(Iterable<int> ruleIds) async {
|
||||
await rules.deleteWhere((t) => t.id.isIn(ruleIds));
|
||||
}
|
||||
|
||||
Future<void> _putAll(
|
||||
Iterable<Rule> rules, {
|
||||
int? profileId,
|
||||
RuleScene? scene,
|
||||
}) async {
|
||||
await batch((b) {
|
||||
b.insertAllOnConflictUpdate(
|
||||
this.rules,
|
||||
rules.map((item) => item.toCompanion()),
|
||||
);
|
||||
b.insertAllOnConflictUpdate(
|
||||
profileRuleLinks,
|
||||
rules.map(
|
||||
(item) => ProfileRuleLink(
|
||||
ruleId: item.id,
|
||||
profileId: profileId,
|
||||
scene: scene,
|
||||
).toCompanion(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _set(
|
||||
Iterable<Rule> rules, {
|
||||
int? profileId,
|
||||
RuleScene? scene,
|
||||
}) async {
|
||||
await batch((b) {
|
||||
b.insertAllOnConflictUpdate(
|
||||
this.rules,
|
||||
rules.map((item) => item.toCompanion()),
|
||||
);
|
||||
|
||||
b.deleteWhere(
|
||||
profileRuleLinks,
|
||||
(t) =>
|
||||
(profileId == null
|
||||
? t.profileId.isNull()
|
||||
: t.profileId.equals(profileId)) &
|
||||
(scene == null ? const Constant(true) : t.scene.equalsValue(scene)),
|
||||
);
|
||||
|
||||
b.insertAllOnConflictUpdate(
|
||||
profileRuleLinks,
|
||||
rules.map(
|
||||
(item) => ProfileRuleLink(
|
||||
ruleId: item.id,
|
||||
profileId: profileId,
|
||||
scene: scene,
|
||||
).toCompanion(),
|
||||
),
|
||||
);
|
||||
|
||||
b.deleteWhere(this.rules, (r) {
|
||||
final linkedIds = selectOnly(profileRuleLinks);
|
||||
linkedIds.addColumns([profileRuleLinks.ruleId]);
|
||||
return r.id.isNotInQuery(linkedIds);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension RawRuleExt on RawRule {
|
||||
Rule toRule([String? order]) {
|
||||
return Rule(id: id, value: value, order: order);
|
||||
}
|
||||
}
|
||||
|
||||
extension RulesCompanionExt on Rule {
|
||||
RulesCompanion toCompanion() {
|
||||
return RulesCompanion.insert(id: Value(id), value: value);
|
||||
}
|
||||
}
|
||||