Compare commits

..

7 Commits

Author SHA1 Message Date
chen08209
6e404ab19c Fix windows some issues
Optimize overwrite handle

Optimize access control page

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

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

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

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-23 15:23:58 +08:00
217 changed files with 11700 additions and 5070 deletions

View File

@@ -22,14 +22,14 @@ jobs:
os: ubuntu-22.04
arch: amd64
- platform: macos
os: macos-13
os: macos-15-intel
arch: amd64
- platform: macos
os: macos-latest
arch: arm64
- platform: windows
os: windows-11-arm
arch: arm64
# - platform: windows
# os: windows-11-arm
# arch: arm64
- platform: linux
os: ubuntu-24.04-arm
arch: arm64
@@ -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 }}

16
.gitignore vendored
View File

@@ -45,11 +45,19 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android/**/.cxx
/android/**/build
/android/common/**/.**/
/android/common/local.*
/android/core/**/includes/
/android/core/**/cmake-build-*/
/android/core/**/jniLibs/
#libclash
#FlClash
/libclash/
#jniLibs
/android/app/src/main/jniLibs/
/services/helper/target
/macos/**/Package.resolved
devtools_options.yaml

4
.gitmodules vendored
View File

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

View File

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

View File

@@ -11,7 +11,6 @@
<service
android:name=".TileService"
android:label="FlClash Debug"
tools:replace="android:label"
tools:targetApi="24" />
tools:replace="android:label" />
</application>
</manifest>

View File

@@ -103,9 +103,8 @@
android:exported="true"
android:permission="${applicationId}.permission.RECEIVE_BROADCASTS">
<intent-filter>
<action android:name="${applicationId}.intent.action.START" />
<action android:name="${applicationId}.intent.action.STOP" />
<action android:name="${applicationId}.intent.action.TOGGLE" />
<action android:name="${applicationId}.intent.action.SERVICE_CREATED" />
<action android:name="${applicationId}.intent.action.SERVICE_DESTROYED" />
</intent-filter>
</receiver>

View File

@@ -3,9 +3,6 @@ package com.follow.clash
import android.app.Application
import android.content.Context
import com.follow.clash.common.GlobalState
import com.follow.clash.common.processName
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
class Application : Application() {
@@ -13,21 +10,4 @@ class Application : Application() {
super.attachBaseContext(base)
GlobalState.init(this)
}
override fun onCreate() {
super.onCreate()
initRemoteCrashlytics(this)
}
private fun initRemoteCrashlytics(context: Context) {
try {
if (processName?.endsWith(":remote") == true) {
FirebaseApp.initializeApp(context)
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = true
GlobalState.log("init remote crashlytics")
}
} catch (e: Exception) {
GlobalState.log("initRemoteCrashlytics error: $e")
}
}
}

View File

@@ -4,29 +4,24 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.GlobalState
import com.follow.clash.common.action
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class BroadcastReceiver : BroadcastReceiver(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
class BroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BroadcastAction.START.action -> {
launch {
BroadcastAction.SERVICE_CREATED.action -> {
GlobalState.log("Receiver service created")
GlobalState.launch {
State.handleStartServiceAction()
}
}
BroadcastAction.STOP.action -> {
State.handleStopServiceAction()
}
BroadcastAction.TOGGLE.action -> {
launch {
State.handleToggleAction()
BroadcastAction.SERVICE_DESTROYED.action -> {
GlobalState.log("Receiver service destroyed")
GlobalState.launch {
State.handleStopServiceAction()
}
}
}

View File

@@ -32,9 +32,8 @@ suspend fun PackageManager.getPackageIconPath(packageName: String): String =
if (iconFile.exists() && !isExpired(iconFile)) {
return@withContext iconFile.absolutePath
}
iconDir.listFiles()?.forEach { file ->
if (file.name.startsWith(packageName + "_")) file.delete()
}
iconDir.listFiles { f -> f.name.startsWith("${packageName}_") }?.forEach(File::delete)
val icon = getApplicationIcon(packageName)
saveDrawableToFile(icon, iconFile)
iconFile.absolutePath
@@ -47,8 +46,10 @@ suspend fun PackageManager.getPackageIconPath(packageName: String): String =
}
}
private fun saveDrawableToFile(drawable: Drawable, file: File) {
val bitmap = drawable.toBitmap()
private suspend fun saveDrawableToFile(drawable: Drawable, file: File) {
val bitmap = withContext(Dispatchers.Default) {
drawable.toBitmap(width = 128, height = 128)
}
try {
val format = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {

View File

@@ -2,6 +2,7 @@ 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
import com.follow.clash.plugins.TilePlugin
@@ -31,6 +32,9 @@ class MainActivity : FlutterActivity(),
}
override fun onDestroy() {
GlobalState.launch {
Service.setEventListener(null)
}
State.flutterEngine = null
super.onDestroy()
}

View File

@@ -3,12 +3,17 @@ package com.follow.clash
import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.formatString
import com.follow.clash.common.intent
import com.follow.clash.service.IAckInterface
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.RemoteService
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object Service {
private val delegate by lazy {
@@ -39,10 +44,12 @@ object Service {
val res = mutableListOf<ByteArray>()
return delegate.useService {
it.invokeAction(
data,
object : ICallbackInterface.Stub() {
override fun onResult(result: ByteArray?, isSuccess: Boolean) {
data, object : ICallbackInterface.Stub() {
override fun onResult(
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
res.add(result ?: byteArrayOf())
ack?.onAck()
if (isSuccess) {
cb(res.formatString())
}
@@ -52,23 +59,29 @@ object Service {
}
suspend fun setEventListener(
cb: (result: String?) -> Unit
cb: ((result: String?) -> Unit)?
): Result<Unit> {
val results = HashMap<String, MutableList<ByteArray>>()
return delegate.useService {
it.setEventListener(object : IEventInterface.Stub() {
override fun onEvent(
id: String, data: ByteArray?, isSuccess: Boolean
) {
if (results[id] == null) {
results[id] = mutableListOf()
}
results[id]?.add(data ?: byteArrayOf())
if (isSuccess) {
cb(results[id]?.formatString())
results.remove(id)
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)
}
}
}
false -> null
})
}
}
@@ -81,11 +94,54 @@ object Service {
}
}
suspend fun startService(options: VpnOptions, inApp: Boolean) {
delegate.useService { it.startService(options, inApp) }
suspend fun setCrashlytics(
enable: Boolean
): Result<Unit> {
return delegate.useService {
it.setCrashlytics(enable)
}
}
suspend fun stopService() {
delegate.useService { it.stopService() }
private suspend fun awaitIResultInterface(
block: (IResultInterface) -> Unit
): Long = suspendCancellableCoroutine { continuation ->
val callback = object : IResultInterface.Stub() {
override fun onResult(time: Long) {
if (continuation.isActive) {
continuation.resume(time)
}
}
}
try {
block(callback)
} catch (e: Exception) {
if (continuation.isActive) {
continuation.resumeWithException(e)
}
}
}
suspend fun startService(options: VpnOptions, runTime: Long): Long {
return delegate.useService {
awaitIResultInterface { callback ->
it.startService(options, runTime, callback)
}
}.getOrNull() ?: 0L
}
suspend fun stopService(): Long {
return delegate.useService {
awaitIResultInterface { callback ->
it.stopService(callback)
}
}.getOrNull() ?: 0L
}
suspend fun getRunTime(): Long {
return delegate.useService {
it.runTime
}.getOrNull() ?: 0L
}
}

View File

@@ -26,6 +26,7 @@ object State {
var runTime: Long = 0
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
var flutterEngine: FlutterEngine? = null
var serviceFlutterEngine: FlutterEngine? = null
@@ -51,25 +52,53 @@ object State {
action?.invoke()
}
suspend fun handleStartServiceAction() {
tilePlugin?.handleStart()
if (flutterEngine != null) {
return
suspend fun handleSyncState() {
runLock.withLock {
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)
}
}
startServiceWithEngine()
}
fun handleStopServiceAction() {
tilePlugin?.handleStop()
if (flutterEngine != null || serviceFlutterEngine != null) {
return
suspend fun handleStartServiceAction() {
runLock.withLock {
if (runStateFlow.value != RunState.STOP) {
return
}
tilePlugin?.handleStart()
if (flutterEngine != null) {
return
}
startServiceWithEngine()
}
}
suspend fun handleStopServiceAction() {
runLock.withLock {
if (runStateFlow.value != RunState.START) {
return
}
tilePlugin?.handleStop()
if (flutterEngine != null || serviceFlutterEngine != null) {
return
}
handleStopService()
}
handleStopService()
}
fun handleStartService() {
val appPlugin = flutterEngine?.plugin<AppPlugin>()
if (appPlugin != null) {
appPlugin?.requestNotificationsPermission {
appPlugin.requestNotificationsPermission {
startService()
}
return
@@ -77,8 +106,23 @@ 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()
@@ -88,61 +132,48 @@ object State {
}
}
suspend fun startServiceWithEngine() {
runLock.withLock {
withContext(Dispatchers.Main) {
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)
private fun startServiceWithEngine() {
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)
}
}
}
}
private fun startService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) {
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
}
val options = servicePlugin?.handleGetVpnOptions() ?: return@launch
appPlugin?.prepare(options.enable) {
runTime = System.currentTimeMillis()
Service.startService(options, true)
runTime = Service.startService(options, runTime)
runStateFlow.tryEmit(RunState.START)
}
}
}
}
fun handleStopService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.STOP) {
return@launch
}
runStateFlow.tryEmit(RunState.PENDING)
Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
runTime = 0
}
destroyServiceEngine()
}
}
}

View File

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

View File

@@ -31,6 +31,7 @@ class TileService : TileService() {
scope?.cancel()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
scope?.launch {
State.handleSyncState()
State.runStateFlow.collect {
updateTile(it)
}
@@ -44,8 +45,7 @@ class TileService : TileService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
@Suppress("DEPRECATION") startActivityAndCollapse(intent)
}
}

View File

@@ -2,7 +2,8 @@ package com.follow.clash.models
data class AppState(
val currentProfileName: String,
val stopText: String,
val onlyStatisticsProxy: Boolean,
val crashlytics: Boolean = true,
val currentProfileName: String = "FlClash",
val stopText: String = "Stop",
val onlyStatisticsProxy: Boolean = false,
)

View File

@@ -57,7 +57,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var vpnPrepareCallback: (suspend () -> Unit)? = null
private var requestNotificationCallback: (() -> Unit)? = null
private val packages = mutableListOf<Package>()
private val skipPrefixList = listOf(

View File

@@ -5,6 +5,7 @@ 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
@@ -37,7 +38,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"init" -> {
handleInit(result)
handleInit(call, result)
}
"shutdown" -> {
@@ -114,28 +115,32 @@ 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)
launch {
val data = call.arguments<String>()!!
val params = Gson().fromJson(data, AppState::class.java)
Service.updateNotificationParams(
NotificationParams(
title = params.currentProfileName,
stopText = params.stopText,
onlyStatisticsProxy = params.onlyStatisticsProxy
)
).onSuccess {
result.success("")
}.onFailure {
result.success(it.message)
}
)
Service.setCrashlytics(params.crashlytics)
result.success("")
}
}
fun handleInit(result: MethodChannel.Result) {
fun handleInit(call: MethodCall, result: MethodChannel.Result) {
Service.bind()
launch {
Service.setEventListener {
handleSendEvent(it)
val needSetEventListener = call.arguments<Boolean>() ?: false
when (needSetEventListener) {
true -> Service.setEventListener {
handleSendEvent(it)
}
false -> Service.setEventListener(null)
}.onSuccess {
result.success("")
}.onFailure {
@@ -147,6 +152,9 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
private fun handleGetRunTime(result: MethodChannel.Result) {
return result.success(State.runTime)
launch {
State.handleSyncState()
result.success(State.runTime)
}
}
}

View File

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

View File

@@ -39,4 +39,7 @@ kotlin {
dependencies {
implementation(libs.androidx.core)
implementation(libs.gson)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics.ndk)
implementation(libs.firebase.analytics)
}

View File

@@ -10,9 +10,8 @@ enum class QuickAction {
}
enum class BroadcastAction {
START,
STOP,
TOGGLE,
SERVICE_CREATED,
SERVICE_DESTROYED,
}
enum class AccessControlMode {

View File

@@ -149,7 +149,7 @@ fun Context.receiveBroadcastFlow(
inline fun <reified T : IBinder> Context.bindServiceFlow(
intent: Intent,
flags: Int = Context.BIND_AUTO_CREATE,
maxRetries: Int = 5,
maxRetries: Int = 10,
retryDelayMillis: Long = 200L
): Flow<Pair<IBinder?, String>> = callbackFlow {
val connection = object : ServiceConnection {
@@ -186,6 +186,7 @@ inline fun <reified T : IBinder> Context.bindServiceFlow(
awaitClose {
Handler(Looper.getMainLooper()).post {
unbindService(connection)
trySend(Pair(null, ""))
}
}
}.retryWhen { cause, attempt ->
@@ -219,7 +220,6 @@ val Long.formatBytes: String
fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List<ByteArray> {
val allBytes = toByteArray(charset)
val total = allBytes.size
val maxBytes = when {
total <= 100 * 1024 -> total
total <= 1024 * 1024 -> 64 * 1024

View File

@@ -3,6 +3,8 @@ package com.follow.clash.common
import android.app.Application
import android.util.Log
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -13,16 +15,16 @@ object GlobalState : CoroutineScope by CoroutineScope(Dispatchers.Default) {
const val NOTIFICATION_ID = 1
val packageName: String
get() = _application.packageName
get() = application.packageName
val RECEIVE_BROADCASTS_PERMISSIONS: String
get() = "${packageName}.permission.RECEIVE_BROADCASTS"
private lateinit var _application: Application
private var _application: Application? = null
val application: Application
get() = _application
get() = _application!!
fun log(text: String) {
@@ -32,4 +34,14 @@ object GlobalState : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun init(application: Application) {
_application = application
}
fun setCrashlytics(enable: Boolean) {
_application?.let {
FirebaseApp.initializeApp(it)
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = enable
if (enable) {
log("init crashlytics ${it.processName}")
}
}
}
}

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean
@@ -44,7 +45,10 @@ class ServiceDelegate<T>(
job = null
_serviceState.value = null
job = launch {
GlobalState.application.bindServiceFlow<IBinder>(intent).collect { handleBind(it) }
runCatching {
GlobalState.application.bindServiceFlow<IBinder>(intent)
.collect { handleBind(it) }
}
}
}
}
@@ -56,7 +60,9 @@ class ServiceDelegate<T>(
withTimeout(timeoutMillis) {
val state = serviceState.filterNotNull().first()
state.first?.let {
block(it)
withContext(Dispatchers.Default) {
block(it)
}
} ?: throw Exception(state.second)
}
}

View File

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

View File

@@ -41,9 +41,13 @@ Java_com_follow_clash_core_Core_invokeAction(JNIEnv *env, jobject thiz, jstring
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_setMessageCallback(JNIEnv *env, jobject thiz, jobject cb) {
const auto interface = new_global(cb);
setMessageCallback(interface);
Java_com_follow_clash_core_Core_setEventListener(JNIEnv *env, jobject thiz, jobject cb) {
if (cb != nullptr) {
const auto interface = new_global(cb);
setEventListener(interface);
} else {
setEventListener(nullptr);
}
}
extern "C"
@@ -169,7 +173,7 @@ Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_setMessageCallback(JNIEnv *env, jobject thiz, jobject cb) {
Java_com_follow_clash_core_Core_setEventListener(JNIEnv *env, jobject thiz, jobject cb) {
}
extern "C"

View File

@@ -84,18 +84,22 @@ data object Core {
)
}
private external fun setMessageCallback(cb: InvokeInterface)
private external fun setEventListener(cb: InvokeInterface?)
fun setMessageCallback(
cb: (result: String?) -> Unit
fun callSetEventListener(
cb: ((result: String?) -> Unit)?
) {
setMessageCallback(
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
when (cb != null) {
true -> setEventListener(
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
false -> setEventListener(null)
}
}
external fun stopTun()

View File

@@ -25,7 +25,7 @@
android:process=":remote">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />
android:value="proxy" />
</service>
<service

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,16 @@ 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.models.VpnOptions;
import com.follow.clash.service.models.NotificationParams;
interface IRemoteInterface {
void invokeAction(in String data, in ICallbackInterface callback);
void updateNotificationParams(in NotificationParams params);
void startService(in VpnOptions options,in boolean inApp);
void stopService();
void startService(in VpnOptions options, in long runTime, in IResultInterface result);
void stopService(in IResultInterface result);
void setEventListener(in IEventInterface event);
void setCrashlytics(in boolean enable);
long getRunTime();
}

View File

@@ -0,0 +1,6 @@
// IResultInterface.aidl
package com.follow.clash.service;
interface IResultInterface {
oneway void onResult(in long runTime);
}

View File

@@ -29,6 +29,11 @@ class CommonService : Service(), IBaseService,
handleCreate()
}
override fun onDestroy() {
handleDestroy()
super.onDestroy()
}
override fun onLowMemory() {
Core.forceGC()
super.onLowMemory()
@@ -45,7 +50,11 @@ class CommonService : Service(), IBaseService,
}
override fun start() {
loader.load()
try {
loader.load()
} catch (_: Exception) {
stop()
}
}
override fun stop() {

View File

@@ -1,15 +1,18 @@
package com.follow.clash.service
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.GlobalState
import com.follow.clash.common.sendBroadcast
interface IBaseService {
fun handleCreate() {
if (!State.inApp) {
BroadcastAction.START.sendBroadcast()
} else {
State.inApp = false
}
GlobalState.log("Service create")
BroadcastAction.SERVICE_CREATED.sendBroadcast()
}
fun handleDestroy() {
GlobalState.log("Service destroy")
BroadcastAction.SERVICE_DESTROYED.sendBroadcast()
}
fun start()

View File

@@ -3,57 +3,73 @@ package com.follow.clash.service
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.follow.clash.common.GlobalState
import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.chunkedForAidl
import com.follow.clash.common.intent
import com.follow.clash.core.Core
import com.follow.clash.service.State.delegate
import com.follow.clash.service.State.intent
import com.follow.clash.service.State.runLock
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.withLock
import java.util.UUID
import kotlin.coroutines.resume
class RemoteService : Service(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
private var delegate: ServiceDelegate<IBaseService>? = null
private var intent: Intent? = null
private fun handleStopService() {
private fun handleStopService(result: IResultInterface) {
launch {
delegate?.useService { service ->
service.stop()
delegate?.unbind()
runLock.withLock {
delegate?.useService { service ->
service.stop()
delegate?.unbind()
}
State.runTime = 0
result.onResult(0)
}
}
}
private fun handleServiceDisconnected(message: String) {
GlobalState.log("Background service disconnected: $message")
intent = null
delegate = null
}
private fun handleStartService() {
private fun handleStartService(runTime: Long, result: IResultInterface) {
launch {
val nextIntent = when (State.options?.enable == true) {
true -> VpnService::class.intent
false -> CommonService::class.intent
}
if (intent != nextIntent) {
delegate?.unbind()
delegate = ServiceDelegate(nextIntent, ::handleServiceDisconnected) { binder ->
when (binder) {
is VpnService.LocalBinder -> binder.getService()
is CommonService.LocalBinder -> binder.getService()
else -> throw IllegalArgumentException("Invalid binder type")
}
runLock.withLock {
val nextIntent = when (State.options?.enable == true) {
true -> VpnService::class.intent
false -> CommonService::class.intent
}
intent = nextIntent
delegate?.bind()
}
delegate?.useService { service ->
service.start()
if (intent != nextIntent) {
delegate?.unbind()
delegate = ServiceDelegate(nextIntent, ::handleServiceDisconnected) { binder ->
when (binder) {
is VpnService.LocalBinder -> binder.getService()
is CommonService.LocalBinder -> binder.getService()
else -> throw IllegalArgumentException("Invalid binder type")
}
}
intent = nextIntent
delegate?.bind()
}
delegate?.useService { service ->
service.start()
}
State.runTime = when (runTime != 0L) {
true -> runTime
false -> System.currentTimeMillis()
}
result.onResult(State.runTime)
}
}
}
@@ -61,11 +77,22 @@ class RemoteService : Service(),
private val binder = object : IRemoteInterface.Stub() {
override fun invokeAction(data: String, callback: ICallbackInterface) {
Core.invokeAction(data) {
runCatching {
val chunks = it?.chunkedForAidl() ?: listOf()
val totalSize = chunks.size
chunks.forEachIndexed { index, chunk ->
callback.onResult(chunk, totalSize - 1 == index)
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)
}
},
)
}
}
}
}
}
@@ -75,33 +102,65 @@ class RemoteService : Service(),
State.notificationParamsFlow.tryEmit(params)
}
override fun startService(
options: VpnOptions, inApp: Boolean
options: VpnOptions,
runtime: Long,
result: IResultInterface,
) {
State.options = options
State.inApp = inApp
handleStartService()
handleStartService(runtime, result)
}
override fun stopService() {
handleStopService()
override fun stopService(result: IResultInterface) {
handleStopService(result)
}
override fun setEventListener(event: IEventInterface) {
Core.setMessageCallback {
runCatching {
val id = UUID.randomUUID().toString()
val chunks = it?.chunkedForAidl() ?: listOf()
val totalSize = chunks.size
chunks.forEachIndexed { index, chunk ->
event.onEvent(id, chunk, totalSize - 1 == index)
override fun setEventListener(eventListener: IEventInterface?) {
GlobalState.log("RemoveEventListener ${eventListener == null}")
when (eventListener != null) {
true -> Core.callSetEventListener {
launch {
runCatching {
val id = UUID.randomUUID().toString()
val chunks = it?.chunkedForAidl() ?: listOf()
for ((index, chunk) in chunks.withIndex()) {
suspendCancellableCoroutine { cont ->
eventListener.onEvent(
id,
chunk,
index == chunks.lastIndex,
object : IAckInterface.Stub() {
override fun onAck() {
cont.resume(Unit)
}
},
)
}
}
}
}
}
false -> Core.callSetEventListener(null)
}
}
override fun setCrashlytics(enable: Boolean) {
GlobalState.setCrashlytics(enable)
}
override fun getRunTime(): Long {
return State.runTime
}
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun onDestroy() {
GlobalState.log("Remote service destroy")
super.onDestroy()
}
}

View File

@@ -1,13 +1,22 @@
package com.follow.clash.service
import android.content.Intent
import com.follow.clash.common.ServiceDelegate
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
object State {
var options: VpnOptions? = null
var inApp: Boolean = false
var notificationParamsFlow: MutableStateFlow<NotificationParams?> = MutableStateFlow(
NotificationParams()
)
val runLock = Mutex()
var runTime: Long = 0L
var delegate: ServiceDelegate<IBaseService>? = null
var intent: Intent? = null
}

View File

@@ -11,8 +11,7 @@ import android.os.RemoteException
import android.util.Log
import androidx.core.content.getSystemService
import com.follow.clash.common.AccessControlMode
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.sendBroadcast
import com.follow.clash.common.GlobalState
import com.follow.clash.core.Core
import com.follow.clash.service.models.VpnOptions
import com.follow.clash.service.models.getIpv4RouteAddress
@@ -44,6 +43,11 @@ class VpnService : SystemVpnService(), IBaseService,
handleCreate()
}
override fun onDestroy() {
handleDestroy()
super.onDestroy()
}
private val connectivity by lazy {
getSystemService<ConnectivityManager>()
}
@@ -107,11 +111,13 @@ class VpnService : SystemVpnService(), IBaseService,
try {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
BroadcastAction.STOP.sendBroadcast()
GlobalState.log("VpnService disconnected")
handleDestroy()
}
return isSuccess
} catch (e: RemoteException) {
throw e
GlobalState.log("VpnService onTransact $e")
return false
}
}
}
@@ -207,6 +213,7 @@ class VpnService : SystemVpnService(), IBaseService,
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
GlobalState.log("Open http proxy")
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1", options.port, options.bypassDomain
@@ -227,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()
}
}

View File

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

View File

@@ -47,9 +47,6 @@ class NotificationModule(private val service: Service) : Module() {
private val scope = CoroutineScope(Dispatchers.Default)
override fun onInstall() {
State.notificationParamsFlow.value?.let {
update(it.extended)
}
scope.launch {
val screenFlow = service.receiveBroadcastFlow {
addAction(Intent.ACTION_SCREEN_ON)
@@ -69,6 +66,12 @@ class NotificationModule(private val service: Service) : Module() {
.collect { (params, _) ->
update(params!!)
}
State.notificationParamsFlow.value?.let {
update(it.extended)
} ?: run {
update(NotificationParams().extended)
}
}
}
@@ -90,12 +93,13 @@ class NotificationModule(private val service: Service) : Module() {
setSmallIcon(R.drawable.ic)
setContentTitle("FlClash")
setContentIntent(intent.toPendingIntent)
setPriority(NotificationCompat.PRIORITY_HIGH)
setCategory(NotificationCompat.CATEGORY_SERVICE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
setShowWhen(false)
setShowWhen(true)
setOnlyAlertOnce(true)
}
}
@@ -105,7 +109,6 @@ class NotificationModule(private val service: Service) : Module() {
with(notificationBuilder) {
setContentTitle(params.title)
setContentText(params.contentText)
setPriority(NotificationCompat.PRIORITY_HIGH)
clearActions()
addAction(
0, params.stopText, QuickAction.STOP.quickIntent.toPendingIntent

View File

@@ -340,6 +340,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",
@@ -390,7 +392,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",
@@ -428,5 +430,41 @@
"restartCoreTip": "Are you sure you want to restart the core?",
"forceRestartCoreTip": "Are you sure you want to force restart the core?",
"dnsHijacking": "DNS hijacking",
"coreStatus": "Core status"
"coreStatus": "Core status",
"dataCollectionTip": "Data Collection Notice",
"dataCollectionContent": "This app uses Firebase Crashlytics to collect crash information to improve app stability.\nThe collected data includes device information and crash details, but does not contain personal sensitive data.\nYou can disable this feature in settings.",
"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",
"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?"
}

View File

@@ -340,6 +340,8 @@
"none": "なし",
"basicConfig": "基本設定",
"basicConfigDesc": "基本設定をグローバルに変更",
"advancedConfig": "高度な設定",
"advancedConfigDesc": "多様な設定を提供",
"selectedCountTitle": "{count} 項目が選択されています",
"addRule": "ルールを追加",
"ruleName": "ルール名",
@@ -391,7 +393,7 @@
"existsTip": "現在の{label}は既に存在しています",
"deleteTip": "現在の{label}を削除してもよろしいですか?",
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
"nullTip": "現在{label}はありません",
"nullTip": "まだ{label}はありません",
"script": "スクリプト",
"color": "カラー",
"rename": "リネーム",
@@ -429,5 +431,41 @@
"restartCoreTip": "コアを再起動してもよろしいですか?",
"forceRestartCoreTip": "コアを強制再起動してもよろしいですか?",
"dnsHijacking": "DNSハイジャッキング",
"coreStatus": "コアステータス"
"coreStatus": "コアステータス",
"dataCollectionTip": "データ収集説明",
"dataCollectionContent": "本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。",
"crashlytics": "クラッシュ分析",
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
"appendSystemDns": "システムDNSを追加",
"appendSystemDnsTip": "設定にシステムDNSを強制的に追加します",
"editRule": "ルールを編集",
"overrideMode": "上書きモード",
"standardModeDesc": "標準モード、基本設定を上書きし、シンプルなルール追加機能を提供",
"scriptModeDesc": "スクリプトモード、外部拡張スクリプトを使用し、ワンクリックで設定を上書きする機能を提供",
"addedRules": "追加ルール",
"controlGlobalAddedRules": "グローバル追加ルールを制御",
"overrideScript": "上書きスクリプト",
"goToConfigureScript": "スクリプト設定に移動",
"editGlobalRules": "グローバルルールを編集",
"externalFetch": "外部取得",
"confirmForceCrashCore": "コアを強制的にクラッシュさせてもよろしいですか?",
"confirmClearAllData": "すべてのデータをクリアしてもよろしいですか?",
"loading": "読み込み中...",
"loadTest": "読み込みテスト",
"yearsAgo": "{count}年前",
"monthsAgo": "{count}ヶ月前",
"daysAgo": "{count}日前",
"hoursAgo": "{count}時間前",
"minutesAgo": "{count}分前",
"justNow": "たった今",
"noLongerRemind": "今後表示しない",
"accessControlSettings": "アクセス制御設定",
"turnOn": "オン",
"turnOff": "オフ",
"coreConfigChangeDetected": "コア設定の変更が検出されました",
"reload": "リロード",
"vpnConfigChangeDetected": "VPN設定の変更が検出されました",
"restart": "再起動",
"speedStatistics": "速度統計",
"resetPageChangesTip": "現在のページに変更があります。リセットしてもよろしいですか?"
}

View File

@@ -340,6 +340,8 @@
"none": "Нет",
"basicConfig": "Базовая конфигурация",
"basicConfigDesc": "Глобальное изменение базовых настроек",
"advancedConfig": "Расширенная конфигурация",
"advancedConfigDesc": "Предоставляет разнообразные варианты конфигурации",
"selectedCountTitle": "Выбрано {count} элементов",
"addRule": "Добавить правило",
"ruleName": "Название правила",
@@ -391,7 +393,7 @@
"existsTip": "Текущий {label} уже существует",
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
"nullTip": "Сейчас {label} нет",
"nullTip": "{label} пока отсутствуют",
"script": "Скрипт",
"color": "Цвет",
"rename": "Переименовать",
@@ -429,5 +431,41 @@
"restartCoreTip": "Вы уверены, что хотите перезапустить ядро?",
"forceRestartCoreTip": "Вы уверены, что хотите принудительно перезапустить ядро?",
"dnsHijacking": "DNS-перехват",
"coreStatus": "Основной статус"
"coreStatus": "Основной статус",
"dataCollectionTip": "Уведомление о сборе данных",
"dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.",
"crashlytics": "Анализ сбоев",
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
"appendSystemDns": "Добавить системный DNS",
"appendSystemDnsTip": "Принудительно добавить системный DNS к конфигурации",
"editRule": "Редактировать правило",
"overrideMode": "Режим переопределения",
"standardModeDesc": "Стандартный режим, переопределение базовой конфигурации, предоставление возможности простого добавления правил",
"scriptModeDesc": "Режим скрипта, использование внешних расширяющих скриптов, предоставление возможности переопределения конфигурации одним кликом",
"addedRules": "Добавленные правила",
"controlGlobalAddedRules": "Управление глобальными добавленными правилами",
"overrideScript": "Скрипт переопределения",
"goToConfigureScript": "Перейти к настройке скрипта",
"editGlobalRules": "Редактировать глобальные правила",
"externalFetch": "Внешнее получение",
"confirmForceCrashCore": "Вы уверены, что хотите принудительно аварийно завершить работу ядра?",
"confirmClearAllData": "Вы уверены, что хотите очистить все данные?",
"loading": "Загрузка...",
"loadTest": "Тест загрузки",
"yearsAgo": "{count, plural, one{{count} год назад} few{{count} года назад} many{{count} лет назад} other{{count} года назад}}",
"monthsAgo": "{count, plural, one{{count} месяц назад} few{{count} месяца назад} many{{count} месяцев назад} other{{count} месяца назад}}",
"daysAgo": "{count, plural, one{{count} день назад} few{{count} дня назад} many{{count} дней назад} other{{count} дня назад}}",
"hoursAgo": "{count, plural, one{{count} час назад} few{{count} часа назад} many{{count} часов назад} other{{count} часа назад}}",
"minutesAgo": "{count, plural, one{{count} минута назад} few{{count} минуты назад} many{{count} минут назад} other{{count} минуты назад}}",
"justNow": "Только что",
"noLongerRemind": "Больше не напоминать",
"accessControlSettings": "Настройки контроля доступа",
"turnOn": "Включить",
"turnOff": "Выключить",
"coreConfigChangeDetected": "Обнаружено изменение конфигурации ядра",
"reload": "Перезагрузить",
"vpnConfigChangeDetected": "Обнаружено изменение конфигурации VPN",
"restart": "Перезапустить",
"speedStatistics": "Статистика скорости",
"resetPageChangesTip": "На текущей странице есть изменения. Вы уверены, что хотите сбросить?"
}

View File

@@ -340,6 +340,8 @@
"none": "无",
"basicConfig": "基本配置",
"basicConfigDesc": "全局修改基本配置",
"advancedConfig": "进阶配置",
"advancedConfigDesc": "提供多样化配置",
"selectedCountTitle": "已选择 {count} 项",
"addRule": "添加规则",
"ruleName": "规则名称",
@@ -429,5 +431,41 @@
"restartCoreTip": "您确定要重启核心吗?",
"forceRestartCoreTip": "您确定要强制重启核心吗?",
"dnsHijacking": "DNS劫持",
"coreStatus": "核心状态"
"coreStatus": "核心状态",
"dataCollectionTip": "数据收集说明",
"dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情不包含个人敏感数据。\n您可以在设置中关闭此功能。",
"crashlytics": "崩溃分析",
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
"appendSystemDns": "追加系统DNS",
"appendSystemDnsTip": "强制为配置附加系统DNS",
"editRule": "编辑规则",
"overrideMode": "覆写模式",
"standardModeDesc": "标准模式,覆写基本配置,提供简单追加规则能力",
"scriptModeDesc": "脚本模式,使用外部扩展脚本,提供一键覆写配置的能力",
"addedRules": "附加规则",
"controlGlobalAddedRules": "控制全局附加规则",
"overrideScript": "覆写脚本",
"goToConfigureScript": "前往配置脚本",
"editGlobalRules": "编辑全局规则",
"externalFetch": "外部获取",
"confirmForceCrashCore": "确定要强制崩溃核心?",
"confirmClearAllData": "确定要清除所有数据?",
"loading": "加载中...",
"loadTest": "加载测试",
"yearsAgo": "{count} 年前",
"monthsAgo": "{count} 个月前",
"daysAgo": "{count} 天前",
"hoursAgo": "{count} 小时前",
"minutesAgo": "{count} 分钟前",
"justNow": "刚刚",
"noLongerRemind": "不再提示",
"accessControlSettings": "访问控制设置",
"turnOn": "开启",
"turnOff": "关闭",
"coreConfigChangeDetected": "检测到核心配置更改",
"reload": "重载",
"vpnConfigChangeDetected": "检测到VPN相关配置改动",
"restart": "重启",
"speedStatistics": "网速统计",
"resetPageChangesTip": "当前页面存在更改,确定重置吗?"
}

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 700 B

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

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

View File

@@ -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"
@@ -24,6 +25,7 @@ import (
"github.com/metacubex/mihomo/tunnel"
"os"
"path/filepath"
"runtime"
"sync"
)
@@ -262,13 +264,14 @@ func setupConfig(params *SetupParams) error {
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
}

View File

@@ -18,8 +18,8 @@ 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.8.4 // indirect
github.com/enfein/mieru/v3 v3.19.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/enfein/mieru/v3 v3.22.1 // 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
@@ -33,44 +33,47 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.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/hashicorp/yamux v0.1.2 // 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
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a // indirect
github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d // indirect
github.com/metacubex/ascon v0.1.0 // indirect
github.com/metacubex/bart v0.20.5 // indirect
github.com/metacubex/bart v0.26.0 // indirect
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b // indirect
github.com/metacubex/blake3 v0.1.0 // indirect
github.com/metacubex/chacha v0.1.5 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 // indirect
github.com/metacubex/kcp-go v0.0.0-20251105084629-8c93f4bf37be // 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/quic-go v0.55.1-0.20251024060151-bd465f127128 // 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.5 // indirect
github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac // indirect
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb // 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-20251004051927-c45ee18473bb // indirect
github.com/metacubex/sing-shadowsocks v0.2.12 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.6 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.7 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.7 // indirect
github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0 // indirect
github.com/metacubex/sing-tun v0.4.9 // 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-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 // indirect
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 // indirect
github.com/metacubex/smux v0.0.0-20250922175018-15c9a6a78719 // indirect
github.com/metacubex/tfo-go v0.0.0-20251024101424-368b42b59148 // indirect
github.com/metacubex/utls v1.8.3 // 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
@@ -78,27 +81,21 @@ require (
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // 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
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/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

View File

@@ -22,10 +22,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.19.1 h1:19b9kgFC7oJXX9RLEO5Pi1gO6yek5cWlpK7IJVUoE8I=
github.com/enfein/mieru/v3 v3.19.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.22.1 h1:/XGYYXpEhEJlxosmtbpEJkhtRLHB8IToG7LB8kU2ZDY=
github.com/enfein/mieru/v3 v3.22.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -44,7 +44,6 @@ github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hH
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -55,20 +54,19 @@ 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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/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/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
@@ -78,20 +76,22 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc=
github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/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-20250820070344-732c0c9d418a h1:c1QSGpacSeQdBdWcEKZKGuWLcqIG2wxHEygAcXuDwS4=
github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY=
github.com/metacubex/amneziawg-go v0.0.0-20251104174305-5a0e9f7e361d 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.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/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=
@@ -102,43 +102,47 @@ github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQux
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/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-20251105084629-8c93f4bf37be h1:Y7SigZIqfv/+RIA/D7R6EbB9p+brPRoGOM6zobSmRIM=
github.com/metacubex/kcp-go v0.0.0-20251105084629-8c93f4bf37be/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs=
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/quic-go v0.55.1-0.20251024060151-bd465f127128 h1:I1uvJl206/HbkzEAZpLgGkZgUveOZb+P+6oTUj7dN+o=
github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c=
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.5 h1:m5U8iHvRAUxlme3FZlE/LPIGHjU8oMCUzXWGbQQAC1E=
github.com/metacubex/sing v0.5.5/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac h1:wDH/Jh/yqWbzPktqJP+Y1cUG8hchcrzKzUxJiSpnaQs=
github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb h1:U/m3h8lp/j7i8zFgfvScLdZa1/Y8dd74oO7iZaQq80s=
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
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-20251004051927-c45ee18473bb h1:gxrJmnxuEAel+kh3V7ntqkHjURif0xKDu76nzr/BF5Y=
github.com/metacubex/sing-quic v0.0.0-20251004051927-c45ee18473bb/go.mod h1:JK4+PYUKps6pnlicKjsSUAjAcvIUjhorIjdNZGg930M=
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.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98=
github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
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.7 h1:ZDY/W+1c7PeWWKeKRyUo18fySF/TWjB0i5ui81Ar778=
github.com/metacubex/sing-tun v0.4.7/go.mod h1:xHecZRwBnKWe6zG9amAK9cXf91lF6blgjBqm+VvOrmU=
github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0 h1:WZepq4TOZa6WewB8tGAZrrL+bL2R2ivoBzuEgAeolWc=
github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
github.com/metacubex/sing-tun v0.4.9 h1:jY0Yyt8nnN3yQRN/jTxgqNCmGi1dsFdxdIi7pQUlVVU=
github.com/metacubex/sing-tun v0.4.9/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-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 h1:yN3mQ4cT9sPUciw/rO0Isc/8QlO86DB6g9SEMRgQ8Cw=
github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 h1:csEbKOzRAxJXffOeZnnS3/kA/F55JiTbKv5jcYqCXms=
github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U=
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-20251024101424-368b42b59148 h1:Zd0QqciLIhv9MKbGKTPEgN8WUFsgQGA1WJBy6spEnVU=
github.com/metacubex/tfo-go v0.0.0-20251024101424-368b42b59148/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
github.com/metacubex/utls v1.8.3/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=
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49/go.mod h1:MBeEa9IVBphH7vc3LNtW6ZujVXFizotPo3OEiHQ+TNU=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
@@ -160,18 +164,14 @@ 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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/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/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
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=
@@ -191,11 +191,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/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=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@@ -209,14 +205,12 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
gitlab.com/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=
@@ -240,16 +234,13 @@ 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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -263,7 +254,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -12,6 +12,7 @@ import (
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
@@ -21,6 +22,7 @@ import (
"net"
"os"
"runtime"
"runtime/debug"
"sort"
"strconv"
"time"
@@ -33,6 +35,8 @@ var (
)
func handleInitClash(paramsString string) bool {
runLock.Lock()
defer runLock.Unlock()
var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
@@ -69,22 +73,24 @@ func handleGetIsInit() bool {
}
func handleForceGC() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
log.Infoln("[APP] request force GC")
runtime.GC()
if features.Android {
debug.FreeOSMemory()
}
}
func handleShutdown() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
handleForceGC()
isInit = false
return true
}
func handleValidateConfig(bytes []byte) string {
_, err := config.UnmarshalRawConfig(bytes)
func handleValidateConfig(path string) string {
buf, err := readFile(path)
_, err = config.UnmarshalRawConfig(buf)
if err != nil {
return err.Error()
}

View File

@@ -27,7 +27,7 @@ import (
"unsafe"
)
var messageCallback unsafe.Pointer
var eventListener unsafe.Pointer
type TunHandler struct {
listener *sing_tun.Listener
@@ -37,6 +37,8 @@ type TunHandler struct {
}
func (th *TunHandler) start(fd int, stack, address, dns string) {
runLock.Lock()
defer runLock.Unlock()
_ = th.limit.Acquire(context.TODO(), 4)
defer th.limit.Release(4)
th.initHook()
@@ -164,7 +166,7 @@ func (result ActionResult) send() {
}
invokeResult(result.callback, string(data))
if result.Method != messageMethod {
defer releaseObject(result.callback)
releaseObject(result.callback)
}
}
@@ -202,12 +204,12 @@ func startTUN(callback unsafe.Pointer, fd C.int, stackChar, addressChar, dnsChar
return true
}
//export setMessageCallback
func setMessageCallback(callback unsafe.Pointer) {
if messageCallback != nil {
releaseObject(messageCallback)
//export setEventListener
func setEventListener(listener unsafe.Pointer) {
if eventListener != nil || listener == nil {
releaseObject(eventListener)
}
messageCallback = callback
eventListener = listener
}
//export getTotalTraffic
@@ -225,12 +227,12 @@ func getTraffic(onlyStatisticsProxy bool) *C.char {
}
func sendMessage(message Message) {
if messageCallback == nil {
if eventListener == nil {
return
}
result := ActionResult{
Method: messageMethod,
callback: messageCallback,
callback: eventListener,
Data: message,
}
result.send()

View File

@@ -24,15 +24,14 @@ class Application extends ConsumerStatefulWidget {
}
class ApplicationState extends ConsumerState<Application> {
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
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,
},
);
@@ -102,7 +101,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,7 +161,6 @@ class ApplicationState extends ConsumerState<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
_autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel();
await coreController.destroy();
await globalState.appController.savePreferences();

View File

@@ -1,13 +0,0 @@
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/state.dart';
import 'system.dart';
class Android {
Future<void> init() async {
await service?.init();
await service?.syncAndroidState(globalState.getAndroidState());
}
}
final android = system.isAndroid ? Android() : null;

View File

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

View File

@@ -7,6 +7,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);

View File

@@ -5,7 +5,7 @@ import 'package:fl_clash/common/common.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
class LocalImageCacheManager extends CacheManager {
static const key = 'LocalImageCacheData';
static const key = 'ImageCaches';
static final LocalImageCacheManager _instance = LocalImageCacheManager._();
@@ -14,14 +14,7 @@ class LocalImageCacheManager extends CacheManager {
}
LocalImageCacheManager._()
: super(
Config(
key,
stalePeriod: Duration(days: 30),
maxNrOfCacheObjects: 1000,
fileService: _LocalImageCacheFileService(),
),
);
: super(Config(key, fileService: _LocalImageCacheFileService()));
}
class _LocalImageCacheFileService extends FileService {
@@ -47,28 +40,31 @@ class _LocalImageResponse implements FileServiceResponse {
final Response<ResponseBody> _response;
String? _header(String name) {
return _response.headers.value(name);
}
@override
int get statusCode => _response.statusCode ?? 0;
@override
Stream<List<int>> get content =>
_response.data?.stream.transform(uint8ListToListIntConverter) ??
Stream.empty();
_response.data!.stream.transform(uint8ListToListIntConverter);
@override
int? get contentLength => _response.data?.contentLength;
@override
DateTime get validTill {
// Without a cache-control header we keep the file for a week
var ageDuration = const Duration(days: 7);
final controlHeader = _response.headers.value(
HttpHeaders.cacheControlHeader,
);
final controlHeader = _header(HttpHeaders.cacheControlHeader);
if (controlHeader != null) {
final controlSettings = controlHeader.split(',');
for (final setting in controlSettings) {
final sanitizedSetting = setting.trim().toLowerCase();
if (sanitizedSetting == 'no-cache') {
ageDuration = Duration.zero;
}
if (sanitizedSetting.startsWith('max-age=')) {
final validSeconds =
int.tryParse(sanitizedSetting.split('=')[1]) ?? 0;
@@ -78,6 +74,7 @@ class _LocalImageResponse implements FileServiceResponse {
}
}
}
if (ageDuration > const Duration(days: 7)) {
return _receivedTime.add(ageDuration);
}
@@ -85,14 +82,12 @@ class _LocalImageResponse implements FileServiceResponse {
}
@override
String? get eTag => _response.headers.value(HttpHeaders.etagHeader);
String? get eTag => _header(HttpHeaders.etagHeader);
@override
String get fileExtension {
var fileExtension = '';
final contentTypeHeader = _response.headers.value(
HttpHeaders.contentTypeHeader,
);
final contentTypeHeader = _header(HttpHeaders.contentTypeHeader);
if (contentTypeHeader != null) {
final contentType = ContentType.parse(contentTypeHeader);
fileExtension = contentType.fileExtension;

View File

@@ -1,4 +1,3 @@
export 'android.dart';
export 'app_localizations.dart';
export 'cache.dart';
export 'color.dart';
@@ -39,3 +38,4 @@ export 'text.dart';
export 'tray.dart';
export 'utils.dart';
export 'window.dart';
export 'yaml.dart';

View File

@@ -7,7 +7,7 @@ List<Group> computeSort({
required List<Group> groups,
required ProxiesSortType sortType,
required DelayMap delayMap,
required SelectedMap selectedMap,
required Map<String, String> selectedMap,
required String defaultTestUrl,
}) {
return groups.map((group) {
@@ -31,7 +31,7 @@ DelayState computeProxyDelayState({
required String proxyName,
required String testUrl,
required List<Group> groups,
required SelectedMap selectedMap,
required Map<String, String> selectedMap,
required DelayMap delayMap,
}) {
final state = computeRealSelectedProxyState(
@@ -47,7 +47,7 @@ DelayState computeProxyDelayState({
SelectedProxyState computeRealSelectedProxyState(
String proxyName, {
required List<Group> groups,
required SelectedMap selectedMap,
required Map<String, String> selectedMap,
}) {
return _getRealSelectedProxyState(
SelectedProxyState(proxyName: proxyName),
@@ -59,7 +59,7 @@ SelectedProxyState computeRealSelectedProxyState(
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);
@@ -83,7 +83,7 @@ List<Proxy> _sortOfDelay({
required List<Group> groups,
required List<Proxy> proxies,
required DelayMap delayMap,
required SelectedMap selectedMap,
required Map<String, String> selectedMap,
required String testUrl,
}) {
return List.from(proxies)..sort((a, b) {

View File

@@ -1,3 +1,5 @@
// ignore_for_file: constant_identifier_names
import 'dart:math';
import 'dart:ui';
@@ -21,6 +23,12 @@ 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,
);
final defaultTextScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
@@ -61,10 +69,14 @@ const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>();
const ruleListEquality = ListEquality<Rule>();
const scriptEquality = ListEquality<Script>();
const externalProviderListEquality = ListEquality<ExternalProvider>();
const packageListEquality = ListEquality<Package>();
const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryListEquality =
ListEquality<MapEntry<String, String>>();
const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>();
const delayMapEquality = MapEquality<String, Map<String, int?>>();

View File

@@ -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/widget.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;

View File

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

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
class Debouncer {
@@ -36,16 +37,25 @@ class Throttler {
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
bool fire = false,
}) {
final timer = _operations[tag];
if (timer != null) {
return true;
}
_operations[tag] = Timer(duration, () {
_operations[tag]?.cancel();
_operations.remove(tag);
if (fire) {
Function.apply(func, args);
});
_operations[tag] = Timer(duration, () {
_operations[tag]?.cancel();
_operations.remove(tag);
});
} else {
_operations[tag] = Timer(duration, () {
Function.apply(func, args);
_operations[tag]?.cancel();
_operations.remove(tag);
});
}
return false;
}
@@ -59,7 +69,7 @@ Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
Duration delay = midDuration,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
@@ -69,7 +79,7 @@ Future<T> retry<T>({
}
attempts++;
}
throw 'unknown error';
throw 'retry error';
}
final debouncer = Debouncer();

View File

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

View File

@@ -23,10 +23,7 @@ extension IterableExt<T> on Iterable<T> {
}
}
Iterable<T> fill(
int length, {
required T Function(int count) filler,
}) sync* {
Iterable<T> fill(int length, {required T Function(int count) filler}) sync* {
int count = 0;
for (var item in this) {
yield item;
@@ -85,6 +82,31 @@ extension ListExt<T> on List<T> {
if (length > index) return this[index];
return last;
}
T safeLast(T value) {
if (isNotEmpty) {
return last;
}
return value;
}
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);
}
}
}
extension DoubleListExt on List<double> {
@@ -120,4 +142,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;
}
}

View File

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

View File

@@ -20,6 +20,11 @@ mixin AutoDisposeNotifierMixin<T> on AnyNotifier<T, T> {
}
void onUpdate(T value) {}
void update(T Function(T) builder) {
final value = builder(state);
this.value = value;
}
}
mixin AnyNotifierMixin<T> on AnyNotifier<T, T> {

View File

@@ -1,3 +1,4 @@
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';
@@ -32,6 +33,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 +73,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>(
@@ -134,11 +171,15 @@ class CommonPageTransition extends StatefulWidget {
bool allowSnapshotting,
Widget? child,
) {
final Animation<Offset> delegatedPositionAnimation = CurvedAnimation(
final CurvedAnimation animation = CurvedAnimation(
parent: secondaryAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(_kMiddleLeftTween);
);
final Animation<Offset> delegatedPositionAnimation = animation.drive(
_kMiddleLeftTween,
);
animation.dispose();
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
@@ -224,7 +265,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,
]),
),
@@ -275,7 +316,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);

View File

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

View File

@@ -68,7 +68,12 @@ class AppPath {
Future<String> get configFilePath async {
final homeDirPath = await appPath.homeDirPath;
return join(homeDirPath, 'config.json');
return join(homeDirPath, 'config.yaml');
}
Future<String> get validateFilePath async {
final homeDirPath = await appPath.homeDirPath;
return join(homeDirPath, 'temp', 'validate${utils.id}.yaml');
}
Future<String> get sharedPreferencesPath async {

View File

@@ -42,8 +42,7 @@ class Preferences {
Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future;
return await preferences?.setString(configKey, json.encode(config)) ??
false;
return preferences?.setString(configKey, json.encode(config)) ?? false;
}
Future<void> clearClashConfig() async {

View File

@@ -1,3 +1,4 @@
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';
@@ -12,14 +13,14 @@ class CommonPrint {
return _instance!;
}
void log(String? text) {
void log(String? text, {LogLevel logLevel = LogLevel.info}) {
final payload = '[APP] $text';
debugPrint(payload);
if (!globalState.isInit) {
return;
}
globalState.appController.addLog(
Log.app(payload),
Log.app(payload).copyWith(logLevel: logLevel),
);
}
}

View File

@@ -30,16 +30,15 @@ class Request {
);
}
Future<Response> getFileResponseForUrl(String url) async {
final response = await _clashDio.get(
Future<Response<Uint8List>> getFileResponseForUrl(String url) async {
return await _clashDio.get<Uint8List>(
url,
options: Options(responseType: ResponseType.bytes),
);
return response;
}
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),
);
@@ -73,13 +72,13 @@ class Request {
}
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
'https://ipwho.is/': IpInfo.fromIpWhoIsJson,
'https://api.myip.com/': IpInfo.fromMyIpJson,
'https://ipapi.co/json/': IpInfo.fromIpApiCoJson,
'https://ident.me/json/': IpInfo.fromIdentMeJson,
'http://ip-api.com/json/': IpInfo.fromIpAPIJson,
'https://api.ip.sb/geoip/': IpInfo.fromIpSbJson,
'https://ipinfo.io/json/': IpInfo.fromIpInfoIoJson,
'https://ipwho.is': IpInfo.fromIpWhoIsJson,
'https://api.myip.com': IpInfo.fromMyIpJson,
'https://ipapi.co/json': IpInfo.fromIpApiCoJson,
'https://ident.me/json': IpInfo.fromIdentMeJson,
'http://ip-api.com/json': IpInfo.fromIpAPIJson,
'https://api.ip.sb/geoip': IpInfo.fromIpSbJson,
'https://ipinfo.io/json': IpInfo.fromIpInfoIoJson,
};
Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
@@ -92,11 +91,13 @@ class Request {
}
}
final future = dio.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
options: Options(responseType: ResponseType.json),
);
final future = dio
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
options: Options(responseType: ResponseType.json),
)
.timeout(const Duration(seconds: 10));
future
.then((res) {
if (res.statusCode == HttpStatus.ok && res.data != null) {

View File

@@ -11,16 +11,25 @@ extension StringExtension on String {
}
dynamic get splitByMultipleSeparators {
final parts =
split(RegExp(r'[, ;]+')).where((part) => part.isNotEmpty).toList();
final parts = split(
RegExp(r'[, ;]+'),
).where((part) => part.isNotEmpty).toList();
return parts.length > 1 ? parts : this;
}
int compareToLower(String other) {
return toLowerCase().compareTo(
other.toLowerCase(),
);
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 {
@@ -66,9 +75,9 @@ extension StringExtension on String {
return md5.convert(bytes).toString();
}
// bool containsToLower(String target) {
// return toLowerCase().contains(target);
// }
// bool containsToLower(String target) {
// return toLowerCase().contains(target);
// }
}
extension StringExtensionSafe on String? {

View File

@@ -37,7 +37,7 @@ class System {
'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion,
'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion,
String() => 0
String() => 0,
};
}
@@ -104,7 +104,7 @@ class System {
);
final arguments = [
'-c',
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"',
];
final result = await Process.run(shell, arguments);
if (result.exitCode != 0) {
@@ -148,21 +148,25 @@ class Windows {
final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16();
final shellExecute = _shell32.lookupFunction<
Int32 Function(
final shellExecute = _shell32
.lookupFunction<
Int32 Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
Int32 nShowCmd),
int Function(
Int32 nShowCmd,
),
int Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
int nShowCmd)>('ShellExecuteW');
int nShowCmd,
)
>('ShellExecuteW');
final result = shellExecute(
nullptr,
@@ -177,28 +181,31 @@ class Windows {
calloc.free(argumentsPtr);
calloc.free(operationPtr);
commonPrint.log('windows runas: $command $arguments resultCode:$result');
commonPrint.log(
'windows runas: $command $arguments resultCode:$result',
logLevel: LogLevel.warning,
);
if (result < 42) {
if (result <= 32) {
return false;
}
return true;
}
Future<void> _killProcess(int port) async {
final result = await Process.run('netstat', ['-ano']);
final lines = result.stdout.toString().trim().split('\n');
for (final line in lines) {
if (!line.contains(':$port') || !line.contains('LISTENING')) {
continue;
}
final parts = line.trim().split(RegExp(r'\s+'));
final pid = int.tryParse(parts.last);
if (pid != null) {
await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
}
}
}
// Future<void> _killProcess(int port) async {
// final result = await Process.run('netstat', ['-ano']);
// final lines = result.stdout.toString().trim().split('\n');
// for (final line in lines) {
// if (!line.contains(':$port') || !line.contains('LISTENING')) {
// continue;
// }
// final parts = line.trim().split(RegExp(r'\s+'));
// final pid = int.tryParse(parts.last);
// if (pid != null) {
// await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
// }
// }
// }
Future<WindowsHelperServiceStatus> checkService() async {
// final qcResult = await Process.run('sc', ['qc', appHelperService]);
@@ -224,16 +231,18 @@ class Windows {
return true;
}
await _killProcess(helperPort);
final command = [
'/c',
if (status == WindowsHelperServiceStatus.presence) ...[
'sc',
'taskkill',
'/F',
'/IM',
'$appHelperService.exe'
' & '
'sc',
'delete',
appHelperService,
'/force',
'&&',
'&',
],
'sc',
'create',
@@ -248,15 +257,18 @@ class Windows {
final res = runas('cmd.exe', command);
await Future.delayed(
Duration(milliseconds: 300),
await Future.delayed(Duration(milliseconds: 300));
final retryStatus = await retry(
task: checkService,
retryIf: (status) => status != WindowsHelperServiceStatus.running,
delay: commonDuration,
);
return res;
return res && retryStatus == WindowsHelperServiceStatus.running;
}
Future<bool> registerTask(String appName) async {
final taskXml = '''
final taskXml =
'''
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Principals>
@@ -295,8 +307,9 @@ class Windows {
</Task>''';
final taskPath = join(await appPath.tempPath, 'task.xml');
await File(taskPath).create(recursive: true);
await File(taskPath)
.writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
await File(
taskPath,
).writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
final commandLine = [
'/Create',
'/TN',
@@ -305,10 +318,7 @@ class Windows {
'%s',
'/F',
].join(' ');
return runas(
'schtasks',
commandLine.replaceFirst('%s', taskPath),
);
return runas('schtasks', commandLine.replaceFirst('%s', taskPath));
}
}
@@ -337,23 +347,25 @@ class MacOS {
return null;
}
final device = lineSplits[1];
final serviceResult = await Process.run(
'networksetup',
['-listnetworkserviceorder'],
);
final serviceResult = await Process.run('networksetup', [
'-listnetworkserviceorder',
]);
final serviceResultOutput = serviceResult.stdout.toString();
final currentService = serviceResultOutput.split('\n\n').firstWhere(
(s) => s.contains('Device: $device'),
orElse: () => '',
);
final currentService = serviceResultOutput
.split('\n\n')
.firstWhere((s) => s.contains('Device: $device'), orElse: () => '');
if (currentService.isEmpty) {
return null;
}
final currentServiceNameLine = currentService.split('\n').firstWhere(
(line) => RegExp(r'^\(\d+\).*').hasMatch(line),
orElse: () => '');
final currentServiceNameLineSplits =
currentServiceNameLine.trim().split(' ');
final currentServiceNameLine = currentService
.split('\n')
.firstWhere(
(line) => RegExp(r'^\(\d+\).*').hasMatch(line),
orElse: () => '',
);
final currentServiceNameLineSplits = currentServiceNameLine.trim().split(
' ',
);
if (currentServiceNameLineSplits.length < 2) {
return null;
}
@@ -365,10 +377,10 @@ class MacOS {
if (deviceServiceName == null) {
return null;
}
final result = await Process.run(
'networksetup',
['-getdnsservers', deviceServiceName],
);
final result = await Process.run('networksetup', [
'-getdnsservers',
deviceServiceName,
]);
final output = result.stdout.toString().trim();
if (output.startsWith("There aren't any DNS Servers set on")) {
originDns = [];
@@ -400,15 +412,12 @@ class MacOS {
if (nextDns == null) {
return;
}
await Process.run(
'networksetup',
[
'-setdnsservers',
serviceName,
if (nextDns.isNotEmpty) ...nextDns,
if (nextDns.isEmpty) 'Empty',
],
);
await Process.run('networksetup', [
'-setdnsservers',
serviceName,
if (nextDns.isNotEmpty) ...nextDns,
if (nextDns.isEmpty) 'Empty',
]);
}
}

View File

@@ -1,10 +1,9 @@
import 'dart:io';
import 'package:fl_clash/common/utils.dart';
import 'package:fl_clash/common/iterable.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,24 +14,34 @@ import 'system.dart';
import 'window.dart';
class Tray {
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) {
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);
}
}
@@ -43,9 +52,10 @@ class Tray {
if (system.isAndroid) {
return;
}
if (!Platform.isLinux) {
if (!system.isLinux) {
await _updateSystemTray(
brightness: trayState.brightness,
isStart: trayState.isStart,
tunEnable: trayState.tunEnable,
force: focus,
);
}
@@ -65,6 +75,16 @@ class Tray {
checked: false,
);
menuItems.add(startMenuItem);
if (system.isMacOS) {
final speedStatistics = MenuItem.checkbox(
label: appLocalizations.speedStatistics,
onClick: (_) async {
globalState.appController.updateSpeedStatistics();
},
checked: trayState.showTrayTitle,
);
menuItems.add(speedStatistics);
}
menuItems.add(MenuItem.separator());
for (final mode in Mode.values) {
menuItems.add(
@@ -85,13 +105,11 @@ class Tray {
subMenuItems.add(
MenuItem.checkbox(
label: proxy.name,
checked: trayState.selectedMap[group.name] == proxy.name,
checked:
globalState.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 +121,7 @@ class Tray {
menuItems.add(
MenuItem.submenu(
label: group.name,
submenu: Menu(
items: subMenuItems,
),
submenu: Menu(items: subMenuItems),
),
);
}
@@ -159,38 +175,41 @@ class Tray {
menuItems.add(exitMenuItem);
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
if (system.isLinux) {
await _updateSystemTray(
brightness: trayState.brightness,
isStart: trayState.isStart,
tunEnable: trayState.tunEnable,
force: focus,
);
}
updateTrayTitle(
showTrayTitle: trayState.showTrayTitle,
traffic: globalState.appState.traffics.list.safeLast(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));
}
}

View File

@@ -142,16 +142,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) {
@@ -322,12 +315,15 @@ class Utils {
return SingleActivator(trigger, control: control, meta: !control);
}
FutureOr<T> handleWatch<T>(Function function) async {
FutureOr<T> handleWatch<T>({
required Function function,
required void Function(T data, int elapsedMilliseconds) onWatch,
}) async {
if (kDebugMode) {
final stopwatch = Stopwatch()..start();
final res = await function();
stopwatch.stop();
commonPrint.log('耗时:${stopwatch.elapsedMilliseconds} ms');
onWatch(res, stopwatch.elapsedMilliseconds);
return res;
}
return await function();

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart';
@@ -19,13 +20,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 +60,6 @@ class Window {
}
}
}
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true);
});
}
Future<void> show() async {

18
lib/common/yaml.dart Normal file
View 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();

View File

@@ -8,7 +8,6 @@ import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/core/core.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
@@ -23,8 +22,6 @@ import 'common/common.dart';
import 'models/models.dart';
class AppController {
int? lastProfileModified;
final BuildContext context;
final WidgetRef _ref;
@@ -59,7 +56,11 @@ class AppController {
}
void savePreferencesDebounce() {
debouncer.call(FunctionTag.savePreferences, savePreferences);
debouncer.call(
FunctionTag.savePreferences,
savePreferences,
duration: Duration(seconds: 3),
);
}
void changeProxyDebounce(String groupName, String proxyName) {
@@ -68,7 +69,7 @@ class AppController {
String proxyName,
) async {
await changeProxy(groupName: groupName, proxyName: proxyName);
await updateGroups();
updateGroupsDebounce();
}, args: [groupName, proxyName]);
}
@@ -83,17 +84,26 @@ class AppController {
}
}
Future<void> tryStartCore() async {
if (coreController.isCompleted) {
return;
}
globalState.isUserDisconnected = true;
await _connectCore();
await _initCore();
_ref.read(initProvider.notifier).value = true;
if (_ref.read(isStartProvider)) {
await globalState.handleStart();
}
}
Future<void> updateStatus(bool isStart) async {
if (isStart) {
await globalState.appController.tryStartCore();
await globalState.handleStart([updateRunTime, updateTraffic]);
final currentLastModified = await _ref
.read(currentProfileProvider)
?.profileLastModified;
if (currentLastModified == null || lastProfileModified == null) {
addCheckIpNumDebounce();
return;
}
if (currentLastModified <= (lastProfileModified ?? 0)) {
final profileId = _ref.read(currentProfileIdProvider);
final setupState = globalState.getSetupState(profileId);
if (!setupState.needSetup(globalState.lastSetupState)) {
addCheckIpNumDebounce();
return;
}
@@ -300,9 +310,15 @@ class AppController {
}
final realTunEnable = _ref.read(realTunEnableProvider);
final realPatchConfig = patchConfig.copyWith.tun(enable: realTunEnable);
final message = await coreController.setupConfig(realPatchConfig);
lastProfileModified = await _ref.read(
currentProfileProvider.select((state) => state?.profileLastModified),
final currentProfile = _ref.read(currentProfileProvider);
final setupState = _ref.read(setupStateProvider(currentProfile?.id ?? ''));
globalState.lastSetupState = setupState;
if (system.isAndroid) {
globalState.lastVpnState = _ref.read(vpnStateProvider);
}
final message = await globalState.setupConfig(
setupState: setupState,
patchConfig: realPatchConfig,
);
if (message.isNotEmpty) {
throw message;
@@ -310,7 +326,6 @@ class AppController {
}
Future _applyProfile() async {
await coreController.requestGc();
await setupClashConfig();
await updateGroups();
await updateProviders();
@@ -354,13 +369,14 @@ class AppController {
try {
await updateProfile(profile);
} catch (e) {
commonPrint.log(e.toString());
commonPrint.log(e.toString(), logLevel: LogLevel.warning);
}
}
}
Future<void> updateGroups() async {
try {
commonPrint.log('updateGroups');
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
final sortType = _ref.read(
@@ -437,15 +453,15 @@ class AppController {
}
Future<void> handleExit() async {
Future.delayed(commonDuration, () {
Future.delayed(Duration(seconds: 3), () {
system.exit();
});
try {
await savePreferences();
await macOS?.updateDns(true);
await proxy?.stopProxy();
await coreController.shutdown();
await macOS?.updateDns(true);
await coreController.destroy();
commonPrint.log('exit');
} finally {
system.exit();
}
@@ -465,11 +481,8 @@ class AppController {
Future<void> checkUpdateResultHandle({
Map<String, dynamic>? data,
bool handleError = false,
bool isUser = false,
}) async {
if (globalState.isPre) {
return;
}
if (data != null) {
final tagName = data['tag_name'];
final body = data['body'];
@@ -487,12 +500,16 @@ class AppController {
],
),
confirmText: appLocalizations.goDownload,
cancelText: isUser ? null : appLocalizations.noLongerRemind,
);
if (res != true) {
return;
if (res == true) {
launchUrl(Uri.parse('https://github.com/$repository/releases/latest'));
} else if (!isUser && res == false) {
_ref
.read(appSettingProvider.notifier)
.update((state) => state.copyWith(autoCheckUpdate: false));
}
launchUrl(Uri.parse('https://github.com/$repository/releases/latest'));
} else if (handleError) {
} else if (isUser) {
globalState.showMessage(
title: appLocalizations.checkUpdate,
message: TextSpan(text: appLocalizations.checkUpdateError),
@@ -530,6 +547,7 @@ class AppController {
FlutterError.onError = (details) {
commonPrint.log(
'exception: ${details.exception} stack: ${details.stack}',
logLevel: LogLevel.warning,
);
};
updateTray(true);
@@ -543,8 +561,8 @@ class AppController {
}
await _handlePreference();
await _handlerDisclaimer();
await _showCrashlyticsTip();
await _connectCore();
await service?.syncAndroidState(globalState.getAndroidState());
await _initCore();
await _initStatus();
_ref.read(initProvider.notifier).value = true;
@@ -552,7 +570,11 @@ class AppController {
Future<void> _connectCore() async {
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
final message = await coreController.preload();
final result = await Future.wait([
coreController.preload(),
if (!globalState.isService) Future.delayed(Duration(milliseconds: 300)),
]);
final String message = result[0];
if (message.isNotEmpty) {
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
if (context.mounted) {
@@ -620,7 +642,6 @@ class AppController {
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
context: context,
dismissible: false,
child: CommonDialog(
title: appLocalizations.disclaimer,
@@ -633,30 +654,47 @@ class AppController {
),
TextButton(
onPressed: () {
_ref
.read(appSettingProvider.notifier)
.updateState(
(state) => state.copyWith(disclaimerAccepted: true),
);
Navigator.of(context).pop<bool>(true);
},
child: Text(appLocalizations.agree),
),
],
child: SelectableText(appLocalizations.disclaimerDesc),
child: Text(appLocalizations.disclaimerDesc),
),
) ??
false;
}
Future<void> _showCrashlyticsTip() async {
if (!system.isAndroid) {
return;
}
if (_ref.read(appSettingProvider.select((state) => state.crashlyticsTip))) {
return;
}
await globalState.showMessage(
title: appLocalizations.dataCollectionTip,
cancelable: false,
message: TextSpan(text: appLocalizations.dataCollectionContent),
);
_ref
.read(appSettingProvider.notifier)
.updateState((state) => state.copyWith(crashlyticsTip: true));
}
Future<void> _handlerDisclaimer() async {
if (_ref.read(appSettingProvider).disclaimerAccepted) {
if (_ref.read(
appSettingProvider.select((state) => state.disclaimerAccepted),
)) {
return;
}
final isDisclaimerAccepted = await showDisclaimer();
if (!isDisclaimerAccepted) {
await handleExit();
}
_ref
.read(appSettingProvider.notifier)
.updateState((state) => state.copyWith(disclaimerAccepted: true));
return;
}
@@ -763,11 +801,17 @@ class AppController {
updateStatus(!_ref.read(isStartProvider));
}
void updateSpeedStatistics() {
_ref
.read(appSettingProvider.notifier)
.update((state) => state.copyWith(showTrayTitle: !state.showTrayTitle));
}
void updateCurrentSelectedMap(String groupName, String proxyName) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null &&
currentProfile.selectedMap[groupName] != proxyName) {
final SelectedMap selectedMap = Map.from(currentProfile.selectedMap)
final selectedMap = Map<String, String>.from(currentProfile.selectedMap)
..[groupName] = proxyName;
_ref
.read(profilesProvider.notifier)
@@ -872,7 +916,7 @@ class AppController {
json.decode(utf8.decode(configFile.content)),
);
for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name);
final filePath = join(homeDirPath, posix.normalize(profile.name));
final file = File(filePath);
await file.create(recursive: true);
await file.writeAsBytes(profile.content);
@@ -919,14 +963,27 @@ class AppController {
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
_ref.read(scriptStateProvider.notifier).value = config.scriptProps;
_ref.read(scriptsProvider.notifier).value = config.scripts;
_ref.read(rulesProvider.notifier).value = config.rules;
}
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
if (currentProfile == null && profiles.isNotEmpty) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
}
void checkNeedSetup() {
if (!globalState.isStart) {
return;
}
final profileId = _ref.read(currentProfileIdProvider);
final setupState = globalState.getSetupState(profileId);
if (!setupState.needSetup(globalState.lastSetupState)) {
return;
}
setupClashConfigDebounce();
}
Future<T?> safeRun<T>(
FutureOr<T> Function() futureFunction, {
String? title,
@@ -936,12 +993,12 @@ class AppController {
final realSilence = needLoading == true ? true : silence;
try {
if (needLoading) {
_ref.read(loadingProvider.notifier).value = true;
_ref.read(loadingProvider.notifier).start();
}
final res = await futureFunction();
return res;
} catch (e) {
commonPrint.log('$futureFunction ===> $e');
commonPrint.log('$title===> $e', logLevel: LogLevel.warning);
if (realSilence) {
globalState.showNotifier(e.toString());
} else {
@@ -952,7 +1009,7 @@ class AppController {
}
return null;
} finally {
_ref.read(loadingProvider.notifier).value = false;
_ref.read(loadingProvider.notifier).stop();
}
}
}

View File

@@ -29,6 +29,8 @@ class CoreController {
return _instance!;
}
bool get isCompleted => _interface.completer.isCompleted;
Future<String> preload() {
return _interface.preload();
}
@@ -71,24 +73,42 @@ class CoreController {
FutureOr<bool> get isInit => _interface.isInit;
FutureOr<String> validateConfig(String data) {
return _interface.validateConfig(data);
Future<String> validateConfig(String data) async {
final path = await appPath.validateFilePath;
await globalState.genValidateFile(path, data);
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);
final res = await _interface.validateConfig(path);
await File(path).delete();
return res;
}
Future<String> updateConfig(UpdateParams updateParams) async {
return await _interface.updateConfig(updateParams);
}
Future<String> setupConfig(ClashConfig clashConfig) async {
await globalState.genConfigFile(clashConfig);
final params = await globalState.getSetupParams();
return await _interface.setupConfig(params);
Future<String> setupConfig({
required SetupParams params,
required SetupState setupState,
VoidCallback? preloadInvoke,
}) async {
final res = _interface.setupConfig(params);
if (preloadInvoke != null) {
preloadInvoke();
}
return res;
}
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();

View File

@@ -5,6 +5,7 @@ import 'dart:isolate';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart';
mixin CoreInterface {
Future<bool> init(InitParams params);
@@ -17,7 +18,7 @@ mixin CoreInterface {
Future<bool> forceGc();
Future<String> validateConfig(String data);
Future<String> validateConfig(String path);
Future<Result> getConfig(String path);
@@ -76,7 +77,7 @@ mixin CoreInterface {
}
abstract class CoreHandlerInterface with CoreInterface {
Future get connected;
Completer get completer;
FutureOr<bool> destroy();
@@ -85,8 +86,27 @@ abstract class CoreHandlerInterface with CoreInterface {
dynamic data,
Duration? timeout,
}) async {
await connected;
return invoke(method: method, data: data, timeout: timeout);
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) {
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
}
return utils.handleWatch(
function: () async {
return await invoke(method: method, data: data, timeout: timeout);
},
onWatch: (data, elapsedMilliseconds) {
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
},
);
}
Future<T?> invoke<T>({
@@ -125,10 +145,10 @@ abstract class CoreHandlerInterface with CoreInterface {
}
@override
Future<String> validateConfig(String data) async {
Future<String> validateConfig(String path) async {
return await _invoke<String>(
method: ActionMethod.validateConfig,
data: data,
data: path,
) ??
'';
}
@@ -145,7 +165,7 @@ abstract class CoreHandlerInterface with CoreInterface {
@override
Future<Result> getConfig(String path) async {
return await _invoke<Result>(method: ActionMethod.getConfig, data: path) ??
Result<Map<String, dynamic>>.success({});
Result.success({});
}
@override

View File

@@ -4,6 +4,7 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/state.dart';
import 'interface.dart';
@@ -17,10 +18,14 @@ class CoreLib extends CoreHandlerInterface {
@override
Future<String> preload() async {
final res = await service?.init();
if (res?.isEmpty == true) {
_connectedCompleter.complete(true);
if (res?.isEmpty != true) {
return res ?? '';
}
return res ?? '';
_connectedCompleter.complete(true);
final syncRes = await service?.syncAndroidState(
globalState.getAndroidState(),
);
return syncRes ?? '';
}
factory CoreLib() {
@@ -57,7 +62,7 @@ class CoreLib extends CoreHandlerInterface {
}
@override
Future get connected => _connectedCompleter.future;
Completer get completer => _connectedCompleter;
}
CoreLib? get coreLib => system.isAndroid ? CoreLib() : null;

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