Compare commits
5 Commits
v0.8.88-pr
...
v0.8.90-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f780530be2 | ||
|
|
201062dc5d | ||
|
|
45b163184d | ||
|
|
2ab70f193a | ||
|
|
ed7868282a |
6
.github/workflows/build.yaml
vendored
6
.github/workflows/build.yaml
vendored
@@ -27,9 +27,9 @@ jobs:
|
||||
- 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
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
## 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -44,8 +45,11 @@ object Service {
|
||||
return delegate.useService {
|
||||
it.invokeAction(
|
||||
data, object : ICallbackInterface.Stub() {
|
||||
override fun onResult(result: ByteArray?, isSuccess: Boolean) {
|
||||
override fun onResult(
|
||||
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
|
||||
) {
|
||||
res.add(result ?: byteArrayOf())
|
||||
ack?.onAck()
|
||||
if (isSuccess) {
|
||||
cb(res.formatString())
|
||||
}
|
||||
@@ -55,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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -94,11 +104,11 @@ object Service {
|
||||
|
||||
private suspend fun awaitIResultInterface(
|
||||
block: (IResultInterface) -> Unit
|
||||
): Unit = suspendCancellableCoroutine { continuation ->
|
||||
): Long = suspendCancellableCoroutine { continuation ->
|
||||
val callback = object : IResultInterface.Stub() {
|
||||
override fun onResult() {
|
||||
override fun onResult(time: Long) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(Unit)
|
||||
continuation.resume(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,19 +123,25 @@ object Service {
|
||||
}
|
||||
|
||||
|
||||
suspend fun startService(options: VpnOptions, inApp: Boolean) {
|
||||
delegate.useService {
|
||||
suspend fun startService(options: VpnOptions, runTime: Long): Long {
|
||||
return delegate.useService {
|
||||
awaitIResultInterface { callback ->
|
||||
it.startService(options, inApp, callback)
|
||||
it.startService(options, runTime, callback)
|
||||
}
|
||||
}
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
suspend fun stopService() {
|
||||
delegate.useService {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -52,20 +52,43 @@ object State {
|
||||
action?.invoke()
|
||||
}
|
||||
|
||||
suspend fun handleStartServiceAction() {
|
||||
tilePlugin?.handleStart()
|
||||
if (flutterEngine != null) {
|
||||
return
|
||||
suspend fun handleSyncState() {
|
||||
runLock.withLock {
|
||||
Service.bind()
|
||||
runTime = Service.getRunTime()
|
||||
val runState = when (runTime == 0L) {
|
||||
true -> RunState.STOP
|
||||
false -> RunState.START
|
||||
}
|
||||
runStateFlow.tryEmit(runState)
|
||||
}
|
||||
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() {
|
||||
@@ -78,8 +101,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()
|
||||
@@ -89,17 +127,24 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +152,7 @@ object State {
|
||||
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)
|
||||
@@ -119,30 +164,14 @@ object State {
|
||||
return@launch
|
||||
}
|
||||
appPlugin?.prepare(options.enable) {
|
||||
Service.startService(options, true)
|
||||
runTime = System.currentTimeMillis()
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ class TempActivity : Activity(),
|
||||
}
|
||||
|
||||
QuickAction.STOP.action -> {
|
||||
State.handleStopServiceAction()
|
||||
launch {
|
||||
State.handleStopServiceAction()
|
||||
}
|
||||
}
|
||||
|
||||
QuickAction.TOGGLE.action -> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,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" -> {
|
||||
@@ -131,11 +131,16 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,8 @@ enum class QuickAction {
|
||||
}
|
||||
|
||||
enum class BroadcastAction {
|
||||
START,
|
||||
STOP,
|
||||
TOGGLE,
|
||||
SERVICE_CREATED,
|
||||
SERVICE_DESTROYED,
|
||||
}
|
||||
|
||||
enum class AccessControlMode {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,8 +48,6 @@ class ServiceDelegate<T>(
|
||||
runCatching {
|
||||
GlobalState.application.bindServiceFlow<IBinder>(intent)
|
||||
.collect { handleBind(it) }
|
||||
}.onFailure {
|
||||
handleBind(Pair(null, it.message ?: "Unknown error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// IAckInterface.aidl
|
||||
package com.follow.clash.service;
|
||||
|
||||
import com.follow.clash.service.IAckInterface;
|
||||
|
||||
interface IAckInterface {
|
||||
oneway void onAck();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +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);
|
||||
}
|
||||
@@ -10,8 +10,9 @@ 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, in IResultInterface result);
|
||||
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();
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface IResultInterface {
|
||||
void onResult();
|
||||
oneway void onResult(in long runTime);
|
||||
}
|
||||
@@ -29,6 +29,11 @@ class CommonService : Service(), IBaseService,
|
||||
handleCreate()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
handleDestroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Core.forceGC()
|
||||
super.onLowMemory()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -8,67 +8,91 @@ 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(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)
|
||||
}
|
||||
result.onResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleServiceDisconnected(message: String) {
|
||||
GlobalState.log("Background service disconnected: $message")
|
||||
intent = null
|
||||
delegate = null
|
||||
}
|
||||
|
||||
private fun handleStartService(result: IResultInterface) {
|
||||
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()
|
||||
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)
|
||||
}
|
||||
delegate?.useService { service ->
|
||||
service.start()
|
||||
}
|
||||
result.onResult()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,37 +102,65 @@ class RemoteService : Service(),
|
||||
State.notificationParamsFlow.tryEmit(params)
|
||||
}
|
||||
|
||||
|
||||
override fun startService(
|
||||
options: VpnOptions, inApp: Boolean, result: IResultInterface
|
||||
options: VpnOptions,
|
||||
runtime: Long,
|
||||
result: IResultInterface,
|
||||
) {
|
||||
State.options = options
|
||||
State.inApp = inApp
|
||||
handleStartService(result)
|
||||
handleStartService(runtime, result)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -432,5 +432,7 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
@@ -433,5 +433,7 @@
|
||||
"dataCollectionTip": "データ収集説明",
|
||||
"dataCollectionContent": "本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。",
|
||||
"crashlytics": "クラッシュ分析",
|
||||
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします"
|
||||
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
|
||||
"appendSystemDns": "システムDNSを追加",
|
||||
"appendSystemDnsTip": "設定にシステムDNSを強制的に追加します"
|
||||
}
|
||||
@@ -433,5 +433,7 @@
|
||||
"dataCollectionTip": "Уведомление о сборе данных",
|
||||
"dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.",
|
||||
"crashlytics": "Анализ сбоев",
|
||||
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя"
|
||||
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
|
||||
"appendSystemDns": "Добавить системный DNS",
|
||||
"appendSystemDnsTip": "Принудительно добавить системный DNS к конфигурации"
|
||||
}
|
||||
@@ -433,5 +433,7 @@
|
||||
"dataCollectionTip": "数据收集说明",
|
||||
"dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情,不包含个人敏感数据。\n您可以在设置中关闭此功能。",
|
||||
"crashlytics": "崩溃分析",
|
||||
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志"
|
||||
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
|
||||
"appendSystemDns": "追加系统DNS",
|
||||
"appendSystemDnsTip": "强制为配置附加系统DNS"
|
||||
}
|
||||
|
||||
Submodule core/Clash.Meta updated: 573489787b...168fc4232c
@@ -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))
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -269,6 +270,7 @@ func setupConfig(params *SetupParams) error {
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup(params.SelectedMap)
|
||||
updateListeners()
|
||||
runtime.GC()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
40
core/go.mod
40
core/go.mod
@@ -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.0 // indirect
|
||||
github.com/enfein/mieru/v3 v3.20.0 // 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
|
||||
@@ -34,43 +34,46 @@ require (
|
||||
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/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-20250902133113-a7f637c14281 // indirect
|
||||
github.com/metacubex/ascon v0.1.0 // indirect
|
||||
github.com/metacubex/bart v0.20.5 // indirect
|
||||
github.com/metacubex/bart v0.24.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-20250923001605-1ba6f691c45b // 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/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-20250909002258-06122df8f231 // 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.8 // 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-20250921095601-b102db4216c0 // indirect
|
||||
github.com/metacubex/utls v1.8.1 // 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,24 +81,19 @@ 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/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
|
||||
|
||||
91
core/go.sum
91
core/go.sum
@@ -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.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.20.0 h1:1ob7pCIVSH5FYFAfYvim8isLW1vBOS4cFOUF9exJS38=
|
||||
github.com/enfein/mieru/v3 v3.20.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/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=
|
||||
@@ -59,16 +58,15 @@ github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
|
||||
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/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-20250902133113-a7f637c14281 h1:09EM0sOLb2kfL0KETGhHujsBLB5iy5U/2yHRHsxf/pI=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20250902133113-a7f637c14281/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY=
|
||||
github.com/metacubex/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.24.0 h1:EyNiPeVOlg0joSHTzi5oentI0j5M89utUq/5dd76pWM=
|
||||
github.com/metacubex/bart v0.24.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,8 +102,10 @@ 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-20250923001605-1ba6f691c45b h1:z7JLKjugnQ1qvDOAD8yMA5C8AlJY3bG+VrrgRntRlUY=
|
||||
github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b/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=
|
||||
@@ -113,32 +115,34 @@ github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFq
|
||||
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-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
|
||||
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.8 h1:3PyiUKWXYi37yHptXskzL1723O3OUdyt0Aej4XHVikM=
|
||||
github.com/metacubex/sing-tun v0.4.8/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-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.8.1 h1:RW8GeCGWAegjV0HW5nw9DoqNoeGAXXeYUP6AysmRvx4=
|
||||
github.com/metacubex/utls v1.8.1/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
||||
github.com/metacubex/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,8 +164,6 @@ 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=
|
||||
@@ -170,8 +172,6 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
|
||||
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/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,8 +205,7 @@ 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=
|
||||
@@ -240,16 +235,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 +255,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=
|
||||
|
||||
20
core/hub.go
20
core/hub.go
@@ -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), ¶ms)
|
||||
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()
|
||||
}
|
||||
|
||||
18
core/lib.go
18
core/lib.go
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -1,16 +1,185 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
class LocalImageResponse extends HttpGetResponse {
|
||||
LocalImageResponse(super.response);
|
||||
class LocalImageCacheManager extends CacheManager {
|
||||
static const key = 'ImageCaches';
|
||||
|
||||
static final LocalImageCacheManager _instance = LocalImageCacheManager._();
|
||||
|
||||
factory LocalImageCacheManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
LocalImageCacheManager._()
|
||||
: super(Config(key, fileService: _LocalImageCacheFileService()));
|
||||
}
|
||||
|
||||
class _LocalImageCacheFileService extends FileService {
|
||||
_LocalImageCacheFileService();
|
||||
|
||||
@override
|
||||
Future<FileServiceResponse> get(
|
||||
String url, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final response = await request.dio.get<ResponseBody>(
|
||||
url,
|
||||
options: Options(headers: headers, responseType: ResponseType.stream),
|
||||
);
|
||||
return _LocalImageResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalImageResponse implements FileServiceResponse {
|
||||
_LocalImageResponse(this._response);
|
||||
|
||||
final DateTime _receivedTime = DateTime.now();
|
||||
|
||||
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);
|
||||
|
||||
@override
|
||||
int? get contentLength => _response.data?.contentLength;
|
||||
|
||||
@override
|
||||
DateTime get validTill {
|
||||
final minValidTill = _receivedTime.add(const Duration(days: 7));
|
||||
if (super.validTill.isBefore(minValidTill)) {
|
||||
return minValidTill;
|
||||
var ageDuration = const Duration(days: 7);
|
||||
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;
|
||||
if (validSeconds > 0) {
|
||||
ageDuration = Duration(seconds: validSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.validTill;
|
||||
|
||||
if (ageDuration > const Duration(days: 7)) {
|
||||
return _receivedTime.add(ageDuration);
|
||||
}
|
||||
return _receivedTime.add(const Duration(days: 7));
|
||||
}
|
||||
|
||||
@override
|
||||
String? get eTag => _header(HttpHeaders.etagHeader);
|
||||
|
||||
@override
|
||||
String get fileExtension {
|
||||
var fileExtension = '';
|
||||
final contentTypeHeader = _header(HttpHeaders.contentTypeHeader);
|
||||
if (contentTypeHeader != null) {
|
||||
final contentType = ContentType.parse(contentTypeHeader);
|
||||
fileExtension = contentType.fileExtension;
|
||||
}
|
||||
return fileExtension;
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentTypeConverter on ContentType {
|
||||
String get fileExtension => mimeTypes[mimeType] ?? '.$subType';
|
||||
}
|
||||
|
||||
const mimeTypes = {
|
||||
'application/vnd.android.package-archive': '.apk',
|
||||
'application/epub+zip': '.epub',
|
||||
'application/gzip': '.gz',
|
||||
'application/java-archive': '.jar',
|
||||
'application/json': '.json',
|
||||
'application/ld+json': '.jsonld',
|
||||
'application/msword': '.doc',
|
||||
'application/octet-stream': '.bin',
|
||||
'application/ogg': '.ogx',
|
||||
'application/pdf': '.pdf',
|
||||
'application/php': '.php',
|
||||
'application/rtf': '.rtf',
|
||||
'application/vnd.amazon.ebook': '.azw',
|
||||
'application/vnd.apple.installer+xml': '.mpkg',
|
||||
'application/vnd.mozilla.xul+xml': '.xul',
|
||||
'application/vnd.ms-excel': '.xls',
|
||||
'application/vnd.ms-fontobject': '.eot',
|
||||
'application/vnd.ms-powerpoint': '.ppt',
|
||||
'application/vnd.oasis.opendocument.presentation': '.odp',
|
||||
'application/vnd.oasis.opendocument.spreadsheet': '.ods',
|
||||
'application/vnd.oasis.opendocument.text': '.odt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
|
||||
'.pptx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||
'.docx',
|
||||
'application/vnd.rar': '.rar',
|
||||
'application/vnd.visio': '.vsd',
|
||||
'application/x-7z-compressed': '.7z',
|
||||
'application/x-abiword': '.abw',
|
||||
'application/x-bzip': '.bz',
|
||||
'application/x-bzip2': '.bz2',
|
||||
'application/x-csh': '.csh',
|
||||
'application/x-freearc': '.arc',
|
||||
'application/x-sh': '.sh',
|
||||
'application/x-shockwave-flash': '.swf',
|
||||
'application/x-tar': '.tar',
|
||||
'application/xhtml+xml': '.xhtml',
|
||||
'application/xml': '.xml',
|
||||
'application/zip': '.zip',
|
||||
'audio/3gpp': '.3gp',
|
||||
'audio/3gpp2': '.3g2',
|
||||
'audio/aac': '.aac',
|
||||
'audio/x-aac': '.aac',
|
||||
'audio/midi': '.midi',
|
||||
'audio/x-midi': '.midi',
|
||||
'audio/x-m4a': '.m4a',
|
||||
'audio/m4a': '.m4a',
|
||||
'audio/mpeg': '.mp3',
|
||||
'audio/ogg': '.oga',
|
||||
'audio/opus': '.opus',
|
||||
'audio/wav': '.wav',
|
||||
'audio/x-wav': '.wav',
|
||||
'audio/webm': '.weba',
|
||||
'font/otf': '.otf',
|
||||
'font/ttf': '.ttf',
|
||||
'font/woff': '.woff',
|
||||
'font/woff2': '.woff2',
|
||||
'image/bmp': '.bmp',
|
||||
'image/gif': '.gif',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/svg+xml': '.svg',
|
||||
'image/tiff': '.tiff',
|
||||
'image/vnd.microsoft.icon': '.ico',
|
||||
'image/webp': '.webp',
|
||||
'text/calendar': '.ics',
|
||||
'text/css': '.css',
|
||||
'text/csv': '.csv',
|
||||
'text/html': '.html',
|
||||
'text/javascript': '.js',
|
||||
'text/plain': '.txt',
|
||||
'text/xml': '.xml',
|
||||
'video/3gpp': '.3gp',
|
||||
'video/3gpp2': '.3g2',
|
||||
'video/mp2t': '.ts',
|
||||
'video/mpeg': '.mpeg',
|
||||
'video/ogg': '.ogv',
|
||||
'video/webm': '.webm',
|
||||
'video/x-msvideo': '.avi',
|
||||
'video/quicktime': '.mov',
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -134,11 +134,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);
|
||||
|
||||
@@ -71,6 +71,11 @@ class AppPath {
|
||||
return join(homeDirPath, 'config.json');
|
||||
}
|
||||
|
||||
Future<String> get validateFilePath async {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return join(homeDirPath, 'temp', 'validate${utils.id}.yaml');
|
||||
}
|
||||
|
||||
Future<String> get sharedPreferencesPath async {
|
||||
final directory = await dataDir.future;
|
||||
return join(directory.path, 'shared_preferences.json');
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +73,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 +92,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) {
|
||||
|
||||
@@ -11,16 +11,15 @@ 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());
|
||||
}
|
||||
|
||||
List<int> get encodeUtf16LeWithBom {
|
||||
@@ -66,9 +65,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? {
|
||||
|
||||
@@ -181,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]);
|
||||
@@ -228,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',
|
||||
@@ -253,8 +258,12 @@ class Windows {
|
||||
final res = runas('cmd.exe', command);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
|
||||
return res;
|
||||
final retryStatus = await retry(
|
||||
task: checkService,
|
||||
retryIf: (status) => status == WindowsHelperServiceStatus.running,
|
||||
delay: commonDuration,
|
||||
);
|
||||
return res && retryStatus == WindowsHelperServiceStatus.running;
|
||||
}
|
||||
|
||||
Future<bool> registerTask(String appName) async {
|
||||
|
||||
@@ -322,12 +322,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();
|
||||
|
||||
@@ -82,8 +82,22 @@ 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)
|
||||
@@ -309,7 +323,6 @@ class AppController {
|
||||
}
|
||||
|
||||
Future _applyProfile() async {
|
||||
await coreController.requestGc();
|
||||
await setupClashConfig();
|
||||
await updateGroups();
|
||||
await updateProviders();
|
||||
@@ -353,7 +366,7 @@ class AppController {
|
||||
try {
|
||||
await updateProfile(profile);
|
||||
} catch (e) {
|
||||
commonPrint.log(e.toString());
|
||||
commonPrint.log(e.toString(), logLevel: LogLevel.warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,9 +479,6 @@ class AppController {
|
||||
Map<String, dynamic>? data,
|
||||
bool handleError = false,
|
||||
}) async {
|
||||
if (globalState.isPre) {
|
||||
return;
|
||||
}
|
||||
if (data != null) {
|
||||
final tagName = data['tag_name'];
|
||||
final body = data['body'];
|
||||
@@ -529,6 +539,7 @@ class AppController {
|
||||
FlutterError.onError = (details) {
|
||||
commonPrint.log(
|
||||
'exception: ${details.exception} stack: ${details.stack}',
|
||||
logLevel: LogLevel.warning,
|
||||
);
|
||||
};
|
||||
updateTray(true);
|
||||
@@ -551,7 +562,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) {
|
||||
@@ -956,7 +971,7 @@ class AppController {
|
||||
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 {
|
||||
|
||||
@@ -29,6 +29,8 @@ class CoreController {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool get isCompleted => _interface.completer.isCompleted;
|
||||
|
||||
Future<String> preload() {
|
||||
return _interface.preload();
|
||||
}
|
||||
@@ -71,18 +73,37 @@ 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 {
|
||||
Future<String> setupConfig(
|
||||
ClashConfig clashConfig, {
|
||||
VoidCallback? preloadInvoke,
|
||||
}) async {
|
||||
await globalState.genConfigFile(clashConfig);
|
||||
final params = await globalState.getSetupParams();
|
||||
return await _interface.setupConfig(params);
|
||||
final res = _interface.setupConfig(params);
|
||||
if (preloadInvoke != null) {
|
||||
preloadInvoke();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<List<Group>> getProxiesGroups({
|
||||
|
||||
@@ -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,19 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
await connected;
|
||||
return invoke(method: method, data: data, timeout: timeout);
|
||||
await completer.future;
|
||||
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 +137,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 +157,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
|
||||
|
||||
@@ -18,11 +18,14 @@ class CoreLib extends CoreHandlerInterface {
|
||||
@override
|
||||
Future<String> preload() async {
|
||||
final res = await service?.init();
|
||||
await service?.syncAndroidState(globalState.getAndroidState());
|
||||
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() {
|
||||
@@ -59,7 +62,7 @@ class CoreLib extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future get connected => _connectedCompleter.future;
|
||||
Completer get completer => _connectedCompleter;
|
||||
}
|
||||
|
||||
CoreLib? get coreLib => system.isAndroid ? CoreLib() : null;
|
||||
|
||||
@@ -38,23 +38,29 @@ class CoreService extends CoreHandlerInterface {
|
||||
completer?.complete(data);
|
||||
}
|
||||
|
||||
void _initServer() {
|
||||
runZonedGuarded(
|
||||
() async {
|
||||
final address = !system.isWindows
|
||||
? InternetAddress(unixSocketPath, type: InternetAddressType.unix)
|
||||
: InternetAddress(localhost, type: InternetAddressType.IPv4);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(address, 0, shared: true);
|
||||
_serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _attachSocket(socket);
|
||||
Future<void> _initServer() async {
|
||||
final server = await retry(
|
||||
task: () async {
|
||||
try {
|
||||
final address = !system.isWindows
|
||||
? InternetAddress(unixSocketPath, type: InternetAddressType.unix)
|
||||
: InternetAddress(localhost, type: InternetAddressType.IPv4);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(address, 0, shared: true);
|
||||
server.listen((socket) async {
|
||||
await _attachSocket(socket);
|
||||
});
|
||||
return server;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
(error, stack) async {
|
||||
commonPrint.log('Service error: $error');
|
||||
},
|
||||
retryIf: (server) => server == null,
|
||||
);
|
||||
if (server == null) {
|
||||
exit(0);
|
||||
}
|
||||
_serverCompleter.complete(server);
|
||||
}
|
||||
|
||||
Future<void> _attachSocket(Socket socket) async {
|
||||
@@ -97,7 +103,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
_process?.stderr.listen((e) {
|
||||
final error = utf8.decode(e);
|
||||
if (error.isNotEmpty) {
|
||||
commonPrint.log(error);
|
||||
commonPrint.log(error, logLevel: LogLevel.warning);
|
||||
}
|
||||
});
|
||||
await _socketCompleter.future;
|
||||
@@ -180,9 +186,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future get connected {
|
||||
return _socketCompleter.future;
|
||||
}
|
||||
Completer get completer => _socketCompleter;
|
||||
}
|
||||
|
||||
final coreService = system.isDesktop ? CoreService() : null;
|
||||
|
||||
@@ -100,6 +100,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"appDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Processing app related settings",
|
||||
),
|
||||
"appendSystemDns": MessageLookupByLibrary.simpleMessage(
|
||||
"Append System DNS",
|
||||
),
|
||||
"appendSystemDnsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Forcefully append system DNS to the configuration",
|
||||
),
|
||||
"application": MessageLookupByLibrary.simpleMessage("Application"),
|
||||
"applicationDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Modify application related settings",
|
||||
|
||||
@@ -80,6 +80,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"app": MessageLookupByLibrary.simpleMessage("アプリ"),
|
||||
"appAccessControl": MessageLookupByLibrary.simpleMessage("アプリアクセス制御"),
|
||||
"appDesc": MessageLookupByLibrary.simpleMessage("アプリ関連設定の処理"),
|
||||
"appendSystemDns": MessageLookupByLibrary.simpleMessage("システムDNSを追加"),
|
||||
"appendSystemDnsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"設定にシステムDNSを強制的に追加します",
|
||||
),
|
||||
"application": MessageLookupByLibrary.simpleMessage("アプリケーション"),
|
||||
"applicationDesc": MessageLookupByLibrary.simpleMessage("アプリ関連設定を変更"),
|
||||
"auto": MessageLookupByLibrary.simpleMessage("自動"),
|
||||
|
||||
@@ -97,6 +97,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"appDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Обработка настроек, связанных с приложением",
|
||||
),
|
||||
"appendSystemDns": MessageLookupByLibrary.simpleMessage(
|
||||
"Добавить системный DNS",
|
||||
),
|
||||
"appendSystemDnsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Принудительно добавить системный DNS к конфигурации",
|
||||
),
|
||||
"application": MessageLookupByLibrary.simpleMessage("Приложение"),
|
||||
"applicationDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"Изменение настроек, связанных с приложением",
|
||||
|
||||
@@ -76,6 +76,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"app": MessageLookupByLibrary.simpleMessage("应用"),
|
||||
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
|
||||
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
|
||||
"appendSystemDns": MessageLookupByLibrary.simpleMessage("追加系统DNS"),
|
||||
"appendSystemDnsTip": MessageLookupByLibrary.simpleMessage("强制为配置附加系统DNS"),
|
||||
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
|
||||
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
|
||||
"auto": MessageLookupByLibrary.simpleMessage("自动"),
|
||||
|
||||
@@ -3358,6 +3358,26 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Append System DNS`
|
||||
String get appendSystemDns {
|
||||
return Intl.message(
|
||||
'Append System DNS',
|
||||
name: 'appendSystemDns',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Forcefully append system DNS to the configuration`
|
||||
String get appendSystemDnsTip {
|
||||
return Intl.message(
|
||||
'Forcefully append system DNS to the configuration',
|
||||
name: 'appendSystemDnsTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -22,6 +22,7 @@ Future<void> main() async {
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _service(List<String> flags) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isService = true;
|
||||
await globalState.init();
|
||||
await coreController.preload();
|
||||
tile?.addListener(
|
||||
@@ -32,16 +33,18 @@ Future<void> _service(List<String> flags) async {
|
||||
},
|
||||
),
|
||||
);
|
||||
Future(() async {
|
||||
app?.tip(appLocalizations.startVpn);
|
||||
final version = await system.version;
|
||||
await coreController.init(version);
|
||||
final clashConfig = globalState.config.patchClashConfig.copyWith.tun(
|
||||
enable: false,
|
||||
);
|
||||
await globalState.handleStart();
|
||||
await coreController.setupConfig(clashConfig);
|
||||
});
|
||||
app?.tip(appLocalizations.startVpn);
|
||||
final version = await system.version;
|
||||
await coreController.init(version);
|
||||
final clashConfig = globalState.config.patchClashConfig.copyWith.tun(
|
||||
enable: false,
|
||||
);
|
||||
coreController.setupConfig(
|
||||
clashConfig,
|
||||
preloadInvoke: () {
|
||||
globalState.handleStart();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
||||
@@ -71,16 +71,16 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
|
||||
@override
|
||||
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
commonPrint.log('$state');
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
globalState.appController.savePreferences();
|
||||
} else {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
render?.resume();
|
||||
}
|
||||
if (state == AppLifecycleState.inactive) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
detectionState.tryStartCheck();
|
||||
});
|
||||
if (system.isAndroid) {
|
||||
globalState.appController.tryStartCore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ class _CoreContainerState extends ConsumerState<CoreManager>
|
||||
|
||||
@override
|
||||
Future<void> onCrash(String message) async {
|
||||
if (!globalState.isUserDisconnected) {
|
||||
if (!globalState.isUserDisconnected &&
|
||||
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
globalState.isUserDisconnected = false;
|
||||
|
||||
@@ -101,7 +101,7 @@ class MessageManagerState extends State<MessageManager> {
|
||||
_cancelMessage(messages.last.id);
|
||||
},
|
||||
child: Card(
|
||||
shape: const RoundedRectangleBorder(
|
||||
shape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(14),
|
||||
),
|
||||
@@ -126,11 +126,13 @@ class MessageManagerState extends State<MessageManager> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
TextButton(
|
||||
IconButton(
|
||||
padding: EdgeInsets.all(2),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
_cancelMessage(messages.last.id);
|
||||
},
|
||||
child: Text(appLocalizations.cancel),
|
||||
icon: Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -87,13 +87,23 @@ class ThemeManager extends ConsumerWidget {
|
||||
top: padding.top > height * 0.3 ? 20.0 : padding.top,
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
globalState.appController.updateViewSize(
|
||||
Size(container.maxWidth, container.maxHeight),
|
||||
);
|
||||
return _buildSystemUi(child);
|
||||
},
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
|
||||
.copyWith(
|
||||
shape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16.0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
globalState.appController.updateViewSize(
|
||||
Size(container.maxWidth, container.maxHeight),
|
||||
);
|
||||
return _buildSystemUi(child);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fl_clash/models/app.dart';
|
||||
import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -5,10 +6,7 @@ import 'package:flutter/material.dart';
|
||||
class TileManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const TileManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
const TileManager({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<TileManager> createState() => _TileContainerState();
|
||||
@@ -21,13 +19,19 @@ class _TileContainerState extends State<TileManager> with TileListener {
|
||||
}
|
||||
|
||||
@override
|
||||
void onStart() {
|
||||
Future<void> onStart() async {
|
||||
if (globalState.appState.isStart) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.updateStatus(true);
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onStop() async {
|
||||
if (!globalState.appState.isStart) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.updateStatus(false);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@@ -24,11 +24,16 @@ class _VpnContainerState extends ConsumerState<VpnManager> {
|
||||
}
|
||||
|
||||
void showTip() {
|
||||
debouncer.call(FunctionTag.vpnTip, () {
|
||||
if (ref.read(isStartProvider)) {
|
||||
globalState.showNotifier(appLocalizations.vpnTip);
|
||||
}
|
||||
});
|
||||
throttler.call(
|
||||
FunctionTag.vpnTip,
|
||||
() {
|
||||
if (ref.read(isStartProvider)) {
|
||||
globalState.showNotifier(appLocalizations.vpnTip);
|
||||
}
|
||||
},
|
||||
duration: const Duration(seconds: 6),
|
||||
fire: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -271,9 +271,11 @@ class AppIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Transform.translate(
|
||||
|
||||
@@ -153,6 +153,7 @@ abstract class NetworkProps with _$NetworkProps {
|
||||
@Default(defaultBypassDomain) List<String> bypassDomain,
|
||||
@Default(RouteMode.config) RouteMode routeMode,
|
||||
@Default(true) bool autoSetSystemDns,
|
||||
@Default(false) bool appendSystemDns,
|
||||
}) = _NetworkProps;
|
||||
|
||||
factory NetworkProps.fromJson(Map<String, Object?>? json) =>
|
||||
|
||||
@@ -192,7 +192,7 @@ extension ActionResultExt on ActionResult {
|
||||
if (code == ResultType.success) {
|
||||
return Result.success(data);
|
||||
} else {
|
||||
return Result.error(data);
|
||||
return Result.error('$data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1199,7 +1199,7 @@ $AccessControlCopyWith<$Res> get accessControl {
|
||||
/// @nodoc
|
||||
mixin _$NetworkProps {
|
||||
|
||||
bool get systemProxy; List<String> get bypassDomain; RouteMode get routeMode; bool get autoSetSystemDns;
|
||||
bool get systemProxy; List<String> get bypassDomain; RouteMode get routeMode; bool get autoSetSystemDns; bool get appendSystemDns;
|
||||
/// Create a copy of NetworkProps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1212,16 +1212,16 @@ $NetworkPropsCopyWith<NetworkProps> get copyWith => _$NetworkPropsCopyWithImpl<N
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is NetworkProps&&(identical(other.systemProxy, systemProxy) || other.systemProxy == systemProxy)&&const DeepCollectionEquality().equals(other.bypassDomain, bypassDomain)&&(identical(other.routeMode, routeMode) || other.routeMode == routeMode)&&(identical(other.autoSetSystemDns, autoSetSystemDns) || other.autoSetSystemDns == autoSetSystemDns));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is NetworkProps&&(identical(other.systemProxy, systemProxy) || other.systemProxy == systemProxy)&&const DeepCollectionEquality().equals(other.bypassDomain, bypassDomain)&&(identical(other.routeMode, routeMode) || other.routeMode == routeMode)&&(identical(other.autoSetSystemDns, autoSetSystemDns) || other.autoSetSystemDns == autoSetSystemDns)&&(identical(other.appendSystemDns, appendSystemDns) || other.appendSystemDns == appendSystemDns));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,systemProxy,const DeepCollectionEquality().hash(bypassDomain),routeMode,autoSetSystemDns);
|
||||
int get hashCode => Object.hash(runtimeType,systemProxy,const DeepCollectionEquality().hash(bypassDomain),routeMode,autoSetSystemDns,appendSystemDns);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NetworkProps(systemProxy: $systemProxy, bypassDomain: $bypassDomain, routeMode: $routeMode, autoSetSystemDns: $autoSetSystemDns)';
|
||||
return 'NetworkProps(systemProxy: $systemProxy, bypassDomain: $bypassDomain, routeMode: $routeMode, autoSetSystemDns: $autoSetSystemDns, appendSystemDns: $appendSystemDns)';
|
||||
}
|
||||
|
||||
|
||||
@@ -1232,7 +1232,7 @@ abstract mixin class $NetworkPropsCopyWith<$Res> {
|
||||
factory $NetworkPropsCopyWith(NetworkProps value, $Res Function(NetworkProps) _then) = _$NetworkPropsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns
|
||||
bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns, bool appendSystemDns
|
||||
});
|
||||
|
||||
|
||||
@@ -1249,12 +1249,13 @@ class _$NetworkPropsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of NetworkProps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? systemProxy = null,Object? bypassDomain = null,Object? routeMode = null,Object? autoSetSystemDns = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? systemProxy = null,Object? bypassDomain = null,Object? routeMode = null,Object? autoSetSystemDns = null,Object? appendSystemDns = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
systemProxy: null == systemProxy ? _self.systemProxy : systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,bypassDomain: null == bypassDomain ? _self.bypassDomain : bypassDomain // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,routeMode: null == routeMode ? _self.routeMode : routeMode // ignore: cast_nullable_to_non_nullable
|
||||
as RouteMode,autoSetSystemDns: null == autoSetSystemDns ? _self.autoSetSystemDns : autoSetSystemDns // ignore: cast_nullable_to_non_nullable
|
||||
as bool,appendSystemDns: null == appendSystemDns ? _self.appendSystemDns : appendSystemDns // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
@@ -1340,10 +1341,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns, bool appendSystemDns)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NetworkProps() when $default != null:
|
||||
return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoSetSystemDns);case _:
|
||||
return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoSetSystemDns,_that.appendSystemDns);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -1361,10 +1362,10 @@ return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoS
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns, bool appendSystemDns) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NetworkProps():
|
||||
return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoSetSystemDns);case _:
|
||||
return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoSetSystemDns,_that.appendSystemDns);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -1381,10 +1382,10 @@ return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoS
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns, bool appendSystemDns)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _NetworkProps() when $default != null:
|
||||
return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoSetSystemDns);case _:
|
||||
return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoSetSystemDns,_that.appendSystemDns);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -1396,7 +1397,7 @@ return $default(_that.systemProxy,_that.bypassDomain,_that.routeMode,_that.autoS
|
||||
@JsonSerializable()
|
||||
|
||||
class _NetworkProps implements NetworkProps {
|
||||
const _NetworkProps({this.systemProxy = true, final List<String> bypassDomain = defaultBypassDomain, this.routeMode = RouteMode.config, this.autoSetSystemDns = true}): _bypassDomain = bypassDomain;
|
||||
const _NetworkProps({this.systemProxy = true, final List<String> bypassDomain = defaultBypassDomain, this.routeMode = RouteMode.config, this.autoSetSystemDns = true, this.appendSystemDns = false}): _bypassDomain = bypassDomain;
|
||||
factory _NetworkProps.fromJson(Map<String, dynamic> json) => _$NetworkPropsFromJson(json);
|
||||
|
||||
@override@JsonKey() final bool systemProxy;
|
||||
@@ -1409,6 +1410,7 @@ class _NetworkProps implements NetworkProps {
|
||||
|
||||
@override@JsonKey() final RouteMode routeMode;
|
||||
@override@JsonKey() final bool autoSetSystemDns;
|
||||
@override@JsonKey() final bool appendSystemDns;
|
||||
|
||||
/// Create a copy of NetworkProps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -1423,16 +1425,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NetworkProps&&(identical(other.systemProxy, systemProxy) || other.systemProxy == systemProxy)&&const DeepCollectionEquality().equals(other._bypassDomain, _bypassDomain)&&(identical(other.routeMode, routeMode) || other.routeMode == routeMode)&&(identical(other.autoSetSystemDns, autoSetSystemDns) || other.autoSetSystemDns == autoSetSystemDns));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NetworkProps&&(identical(other.systemProxy, systemProxy) || other.systemProxy == systemProxy)&&const DeepCollectionEquality().equals(other._bypassDomain, _bypassDomain)&&(identical(other.routeMode, routeMode) || other.routeMode == routeMode)&&(identical(other.autoSetSystemDns, autoSetSystemDns) || other.autoSetSystemDns == autoSetSystemDns)&&(identical(other.appendSystemDns, appendSystemDns) || other.appendSystemDns == appendSystemDns));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,systemProxy,const DeepCollectionEquality().hash(_bypassDomain),routeMode,autoSetSystemDns);
|
||||
int get hashCode => Object.hash(runtimeType,systemProxy,const DeepCollectionEquality().hash(_bypassDomain),routeMode,autoSetSystemDns,appendSystemDns);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NetworkProps(systemProxy: $systemProxy, bypassDomain: $bypassDomain, routeMode: $routeMode, autoSetSystemDns: $autoSetSystemDns)';
|
||||
return 'NetworkProps(systemProxy: $systemProxy, bypassDomain: $bypassDomain, routeMode: $routeMode, autoSetSystemDns: $autoSetSystemDns, appendSystemDns: $appendSystemDns)';
|
||||
}
|
||||
|
||||
|
||||
@@ -1443,7 +1445,7 @@ abstract mixin class _$NetworkPropsCopyWith<$Res> implements $NetworkPropsCopyWi
|
||||
factory _$NetworkPropsCopyWith(_NetworkProps value, $Res Function(_NetworkProps) _then) = __$NetworkPropsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns
|
||||
bool systemProxy, List<String> bypassDomain, RouteMode routeMode, bool autoSetSystemDns, bool appendSystemDns
|
||||
});
|
||||
|
||||
|
||||
@@ -1460,12 +1462,13 @@ class __$NetworkPropsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of NetworkProps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? systemProxy = null,Object? bypassDomain = null,Object? routeMode = null,Object? autoSetSystemDns = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? systemProxy = null,Object? bypassDomain = null,Object? routeMode = null,Object? autoSetSystemDns = null,Object? appendSystemDns = null,}) {
|
||||
return _then(_NetworkProps(
|
||||
systemProxy: null == systemProxy ? _self.systemProxy : systemProxy // ignore: cast_nullable_to_non_nullable
|
||||
as bool,bypassDomain: null == bypassDomain ? _self._bypassDomain : bypassDomain // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,routeMode: null == routeMode ? _self.routeMode : routeMode // ignore: cast_nullable_to_non_nullable
|
||||
as RouteMode,autoSetSystemDns: null == autoSetSystemDns ? _self.autoSetSystemDns : autoSetSystemDns // ignore: cast_nullable_to_non_nullable
|
||||
as bool,appendSystemDns: null == appendSystemDns ? _self.appendSystemDns : appendSystemDns // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ _NetworkProps _$NetworkPropsFromJson(Map<String, dynamic> json) =>
|
||||
$enumDecodeNullable(_$RouteModeEnumMap, json['routeMode']) ??
|
||||
RouteMode.config,
|
||||
autoSetSystemDns: json['autoSetSystemDns'] as bool? ?? true,
|
||||
appendSystemDns: json['appendSystemDns'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$NetworkPropsToJson(_NetworkProps instance) =>
|
||||
@@ -179,6 +180,7 @@ Map<String, dynamic> _$NetworkPropsToJson(_NetworkProps instance) =>
|
||||
'bypassDomain': instance.bypassDomain,
|
||||
'routeMode': _$RouteModeEnumMap[instance.routeMode]!,
|
||||
'autoSetSystemDns': instance.autoSetSystemDns,
|
||||
'appendSystemDns': instance.appendSystemDns,
|
||||
};
|
||||
|
||||
const _$RouteModeEnumMap = {
|
||||
|
||||
@@ -732,7 +732,7 @@ return $default(_that.a,_that.b,_that.c,_that.d);case _:
|
||||
|
||||
|
||||
class _VM4<A,B,C,D> implements VM4<A, B, C, D> {
|
||||
const _VM4({required this.a, required this.b, required this.c, required this.d});
|
||||
const _VM4(this.a, this.b, this.c, this.d);
|
||||
|
||||
|
||||
@override final A a;
|
||||
@@ -789,10 +789,10 @@ class __$VM4CopyWithImpl<A,B,C,D,$Res>
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? a = freezed,Object? b = freezed,Object? c = freezed,Object? d = freezed,}) {
|
||||
return _then(_VM4<A, B, C, D>(
|
||||
a: freezed == a ? _self.a : a // ignore: cast_nullable_to_non_nullable
|
||||
as A,b: freezed == b ? _self.b : b // ignore: cast_nullable_to_non_nullable
|
||||
as B,c: freezed == c ? _self.c : c // ignore: cast_nullable_to_non_nullable
|
||||
as C,d: freezed == d ? _self.d : d // ignore: cast_nullable_to_non_nullable
|
||||
freezed == a ? _self.a : a // ignore: cast_nullable_to_non_nullable
|
||||
as A,freezed == b ? _self.b : b // ignore: cast_nullable_to_non_nullable
|
||||
as B,freezed == c ? _self.c : c // ignore: cast_nullable_to_non_nullable
|
||||
as C,freezed == d ? _self.d : d // ignore: cast_nullable_to_non_nullable
|
||||
as D,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
@@ -174,7 +173,7 @@ extension ProfileExtension on Profile {
|
||||
}
|
||||
|
||||
Future<Profile> saveFile(Uint8List bytes) async {
|
||||
final message = await coreController.validateConfig(utf8.decode(bytes));
|
||||
final message = await coreController.validateConfigFormBytes(bytes);
|
||||
if (message.isNotEmpty) {
|
||||
throw message;
|
||||
}
|
||||
@@ -182,14 +181,4 @@ extension ProfileExtension on Profile {
|
||||
await file.writeAsBytes(bytes);
|
||||
return copyWith(lastUpdateDate: DateTime.now());
|
||||
}
|
||||
|
||||
Future<Profile> saveFileWithString(String value) async {
|
||||
final message = await coreController.validateConfig(value);
|
||||
if (message.isNotEmpty) {
|
||||
throw message;
|
||||
}
|
||||
final file = await getFile();
|
||||
await file.writeAsString(value);
|
||||
return copyWith(lastUpdateDate: DateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ abstract class VM3<A, B, C> with _$VM3<A, B, C> {
|
||||
|
||||
@freezed
|
||||
abstract class VM4<A, B, C, D> with _$VM4<A, B, C, D> {
|
||||
const factory VM4({required A a, required B b, required C c, required D d}) =
|
||||
_VM4;
|
||||
const factory VM4(A a, B b, C c, D d) = _VM4;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -169,8 +169,8 @@ class ScannerOverlay extends CustomPainter {
|
||||
final backgroundPath = Path()..addRect(Rect.largest);
|
||||
|
||||
final cutoutPath = Path()
|
||||
..addRRect(
|
||||
RRect.fromRectAndCorners(
|
||||
..addRSuperellipse(
|
||||
RSuperellipse.fromRectAndCorners(
|
||||
scanWindow,
|
||||
topLeft: Radius.circular(borderRadius),
|
||||
topRight: Radius.circular(borderRadius),
|
||||
@@ -195,7 +195,7 @@ class ScannerOverlay extends CustomPainter {
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4.0;
|
||||
|
||||
final borderRect = RRect.fromRectAndCorners(
|
||||
final border = RSuperellipse.fromRectAndCorners(
|
||||
scanWindow,
|
||||
topLeft: Radius.circular(borderRadius),
|
||||
topRight: Radius.circular(borderRadius),
|
||||
@@ -204,7 +204,7 @@ class ScannerOverlay extends CustomPainter {
|
||||
);
|
||||
|
||||
canvas.drawPath(backgroundWithCutout, backgroundPaint);
|
||||
canvas.drawRRect(borderRect, borderPaint);
|
||||
canvas.drawRSuperellipse(border, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -86,7 +86,11 @@ class Service {
|
||||
}
|
||||
|
||||
Future<String> init() async {
|
||||
return await methodChannel.invokeMethod<String>('init') ?? '';
|
||||
return await methodChannel.invokeMethod<String>(
|
||||
'init',
|
||||
!globalState.isService,
|
||||
) ??
|
||||
'';
|
||||
}
|
||||
|
||||
Future<bool> shutdown() async {
|
||||
|
||||
@@ -2139,11 +2139,11 @@ const needSetupProvider = NeedSetupProvider._();
|
||||
final class NeedSetupProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
VM3<String?, String?, Dns?>,
|
||||
VM3<String?, String?, Dns?>,
|
||||
VM3<String?, String?, Dns?>
|
||||
VM4<String?, String?, Dns?, bool>,
|
||||
VM4<String?, String?, Dns?, bool>,
|
||||
VM4<String?, String?, Dns?, bool>
|
||||
>
|
||||
with $Provider<VM3<String?, String?, Dns?>> {
|
||||
with $Provider<VM4<String?, String?, Dns?, bool>> {
|
||||
const NeedSetupProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
@@ -2160,25 +2160,27 @@ final class NeedSetupProvider
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<VM3<String?, String?, Dns?>> $createElement(
|
||||
$ProviderElement<VM4<String?, String?, Dns?, bool>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
VM3<String?, String?, Dns?> create(Ref ref) {
|
||||
VM4<String?, String?, Dns?, bool> create(Ref ref) {
|
||||
return needSetup(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(VM3<String?, String?, Dns?> value) {
|
||||
Override overrideWithValue(VM4<String?, String?, Dns?, bool> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<VM3<String?, String?, Dns?>>(value),
|
||||
providerOverride: $SyncValueProvider<VM4<String?, String?, Dns?, bool>>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$needSetupHash() => r'3668e8dc9f40a9bea45c94321804eb3afa0e7c51';
|
||||
String _$needSetupHash() => r'25352164c340a5fb02add21246062dd1287595fb';
|
||||
|
||||
@ProviderFor(currentBrightness)
|
||||
const currentBrightnessProvider = CurrentBrightnessProvider._();
|
||||
|
||||
@@ -573,7 +573,7 @@ ColorScheme genColorScheme(
|
||||
}
|
||||
|
||||
@riverpod
|
||||
VM3<String?, String?, Dns?> needSetup(Ref ref) {
|
||||
VM4<String?, String?, Dns?, bool> needSetup(Ref ref) {
|
||||
final profileId = ref.watch(currentProfileIdProvider);
|
||||
final content = ref.watch(
|
||||
scriptStateProvider.select((state) => state.currentScript?.content),
|
||||
@@ -582,7 +582,10 @@ VM3<String?, String?, Dns?> needSetup(Ref ref) {
|
||||
final dns = overrideDns == true
|
||||
? ref.watch(patchClashConfigProvider.select((state) => state.dns))
|
||||
: null;
|
||||
return VM3(a: profileId, b: content, c: dns);
|
||||
final appendSystemDns = ref.watch(
|
||||
networkSettingProvider.select((state) => state.appendSystemDns),
|
||||
);
|
||||
return VM4(profileId, content, dns, appendSystemDns);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
|
||||
@@ -48,6 +48,7 @@ class GlobalState {
|
||||
AppController? _appController;
|
||||
bool isInit = false;
|
||||
bool isUserDisconnected = false;
|
||||
bool isService = false;
|
||||
|
||||
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
||||
|
||||
@@ -238,7 +239,7 @@ class GlobalState {
|
||||
return VpnOptions(
|
||||
stack: config.patchClashConfig.tun.stack.name,
|
||||
enable: vpnProps.enable,
|
||||
systemProxy: networkProps.systemProxy,
|
||||
systemProxy: vpnProps.systemProxy,
|
||||
port: port,
|
||||
ipv6: vpnProps.ipv6,
|
||||
dnsHijacking: vpnProps.dnsHijacking,
|
||||
@@ -303,7 +304,13 @@ class GlobalState {
|
||||
|
||||
Future<void> genConfigFile(ClashConfig pathConfig) async {
|
||||
final configFilePath = await appPath.configFilePath;
|
||||
final config = await patchRawConfig(patchConfig: pathConfig);
|
||||
var config = {};
|
||||
try {
|
||||
config = await patchRawConfig(patchConfig: pathConfig);
|
||||
} catch (e) {
|
||||
globalState.showNotifier(e.toString());
|
||||
config = {};
|
||||
}
|
||||
final res = await Isolate.run<String>(() async {
|
||||
try {
|
||||
final res = json.encode(config);
|
||||
@@ -322,6 +329,42 @@ class GlobalState {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> genValidateFile(String path, String data) async {
|
||||
final res = await Isolate.run<String>(() async {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (!await file.exists()) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
await file.writeAsString(data);
|
||||
return '';
|
||||
} catch (e) {
|
||||
return e.toString();
|
||||
}
|
||||
});
|
||||
if (res.isNotEmpty) {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> genValidateFileFormBytes(String path, Uint8List bytes) async {
|
||||
final res = await Isolate.run<String>(() async {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (!await file.exists()) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
await file.writeAsBytes(bytes);
|
||||
return '';
|
||||
} catch (e) {
|
||||
return e.toString();
|
||||
}
|
||||
});
|
||||
if (res.isNotEmpty) {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
|
||||
AndroidState getAndroidState() {
|
||||
return AndroidState(
|
||||
currentProfileName: config.currentProfile?.label ?? '',
|
||||
@@ -432,10 +475,11 @@ class GlobalState {
|
||||
}
|
||||
final isEnableDns = rawConfig['dns']['enable'] == true;
|
||||
final overrideDns = globalState.config.overrideDns;
|
||||
final systemDns = 'system://';
|
||||
if (overrideDns || !isEnableDns) {
|
||||
final dns = switch (!isEnableDns) {
|
||||
true => realPatchConfig.dns.copyWith(
|
||||
nameserver: [...realPatchConfig.dns.nameserver, 'system://'],
|
||||
nameserver: [...realPatchConfig.dns.nameserver, systemDns],
|
||||
),
|
||||
false => realPatchConfig.dns,
|
||||
};
|
||||
@@ -446,6 +490,12 @@ class GlobalState {
|
||||
entry.value.splitByMultipleSeparators;
|
||||
}
|
||||
}
|
||||
if (config.networkProps.appendSystemDns) {
|
||||
final List<dynamic> nameserver = rawConfig['dns']['nameserver'] ?? [];
|
||||
if (!nameserver.contains(systemDns)) {
|
||||
rawConfig['dns']['nameserver'] = [...nameserver, systemDns];
|
||||
}
|
||||
}
|
||||
List rules = [];
|
||||
if (rawConfig['rules'] != null) {
|
||||
rules = rawConfig['rules'];
|
||||
|
||||
@@ -51,7 +51,7 @@ class UaItem extends ConsumerWidget {
|
||||
subtitle: Text(globalUa ?? appLocalizations.defaultText),
|
||||
delegate: OptionsDelegate<String?>(
|
||||
title: 'UA',
|
||||
options: [null, 'clash-verge/v1.6.6', 'ClashforWindows/0.19.23'],
|
||||
options: [null, 'clash-verge/v2.4.2', 'ClashforWindows/0.19.23'],
|
||||
value: globalUa,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
@@ -257,6 +257,30 @@ class Ipv6Item extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AppendSystemDNSItem extends ConsumerWidget {
|
||||
const AppendSystemDNSItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final appendSystemDNS = ref.watch(
|
||||
networkSettingProvider.select((state) => state.appendSystemDns),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.dns_outlined),
|
||||
title: Text(appLocalizations.appendSystemDns),
|
||||
subtitle: Text(appLocalizations.appendSystemDnsTip),
|
||||
delegate: SwitchDelegate(
|
||||
value: appendSystemDNS,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(networkSettingProvider.notifier)
|
||||
.updateState((state) => state.copyWith(appendSystemDns: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowLanItem extends ConsumerWidget {
|
||||
const AllowLanItem({super.key});
|
||||
|
||||
@@ -437,6 +461,7 @@ final generalItems = <Widget>[
|
||||
Ipv6Item(),
|
||||
AllowLanItem(),
|
||||
UnifiedDelayItem(),
|
||||
AppendSystemDNSItem(),
|
||||
FindProcessItem(),
|
||||
TcpConcurrentItem(),
|
||||
GeodataLoaderItem(),
|
||||
|
||||
@@ -53,7 +53,6 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(commonDuration);
|
||||
globalState.appController.restartCore();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
Future<void> _updateMemory() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final rss = ProcessInfo.currentRss;
|
||||
_memoryStateNotifier.value = await coreController.getMemory() + rss;
|
||||
if (coreController.isCompleted) {
|
||||
_memoryStateNotifier.value = await coreController.getMemory() + rss;
|
||||
} else {
|
||||
_memoryStateNotifier.value = rss;
|
||||
}
|
||||
timer = Timer(Duration(seconds: 2), () async {
|
||||
_updateMemory();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ class StartButton extends ConsumerStatefulWidget {
|
||||
|
||||
class _StartButtonState extends ConsumerState<StartButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
AnimationController? _controller;
|
||||
late Animation<double> _animation;
|
||||
bool isStart = false;
|
||||
|
||||
@@ -28,7 +28,7 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
parent: _controller!,
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
ref.listenManual(runTimeProvider.select((state) => state != null), (
|
||||
@@ -44,7 +44,8 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_controller?.dispose();
|
||||
_controller = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -59,9 +60,9 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
void updateController() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isStart && mounted) {
|
||||
_controller.forward();
|
||||
_controller?.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
_controller?.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -74,12 +75,13 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
}
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
sizeConstraints: BoxConstraints(minWidth: 56, maxWidth: 200),
|
||||
),
|
||||
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
|
||||
.copyWith(
|
||||
sizeConstraints: BoxConstraints(minWidth: 56, maxWidth: 200),
|
||||
),
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
animation: _controller!.view,
|
||||
builder: (_, child) {
|
||||
final textWidth =
|
||||
globalState.measure
|
||||
|
||||
@@ -128,10 +128,11 @@ class TrafficUsage extends StatelessWidget {
|
||||
Container(
|
||||
width: 20,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: primaryColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
3,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -151,10 +152,11 @@ class TrafficUsage extends StatelessWidget {
|
||||
Container(
|
||||
width: 20,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: secondaryColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
3,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/core/controller.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -18,12 +18,14 @@ class DeveloperView extends ConsumerWidget {
|
||||
items: [
|
||||
ListItem(
|
||||
title: Text(appLocalizations.messageTest),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () {
|
||||
context.showNotifier(appLocalizations.messageTestTip);
|
||||
},
|
||||
),
|
||||
ListItem(
|
||||
title: Text(appLocalizations.logsTest),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
globalState.appController.addLog(
|
||||
@@ -36,24 +38,30 @@ class DeveloperView extends ConsumerWidget {
|
||||
),
|
||||
ListItem(
|
||||
title: Text(appLocalizations.crashTest),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () {
|
||||
coreController.crash();
|
||||
// coreController.crash();
|
||||
if (kDebugMode) {
|
||||
coreController.crash();
|
||||
}
|
||||
},
|
||||
),
|
||||
ListItem(
|
||||
title: Text(appLocalizations.clearData),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () async {
|
||||
await globalState.appController.handleClear();
|
||||
},
|
||||
),
|
||||
ListItem(
|
||||
title: Text('Loading'),
|
||||
onTap: () {
|
||||
ref.read(loadingProvider.notifier).value = !ref.read(
|
||||
loadingProvider,
|
||||
);
|
||||
},
|
||||
),
|
||||
// ListItem(
|
||||
// title: Text('Loading'),
|
||||
// minVerticalPadding: 14,
|
||||
// onTap: () {
|
||||
// ref.read(loadingProvider.notifier).value = !ref.read(
|
||||
// loadingProvider,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -488,7 +488,7 @@ class _ListHeaderState extends State<ListHeader> {
|
||||
return CommonCard(
|
||||
enterAnimated: widget.enterAnimated,
|
||||
key: widget.key,
|
||||
radius: 16.ap,
|
||||
radius: 18.ap,
|
||||
type: CommonCardType.filled,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
|
||||
@@ -147,7 +147,8 @@ class ProviderItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(_buildProviderDesc()),
|
||||
if (provider.updateAt.microsecondsSinceEpoch > 0)
|
||||
Text(_buildProviderDesc()),
|
||||
const SizedBox(height: 4),
|
||||
if (provider.subscriptionInfo != null)
|
||||
SubscriptionInfoView(subscriptionInfo: provider.subscriptionInfo),
|
||||
|
||||
@@ -360,7 +360,7 @@ class DelayTestButton extends StatefulWidget {
|
||||
class _DelayTestButtonState extends State<DelayTestButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scale;
|
||||
late Animation<double> _animation;
|
||||
|
||||
Future<void> _healthcheck() async {
|
||||
if (_controller.isAnimating) {
|
||||
@@ -378,10 +378,10 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
_scale = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: const Interval(0, 1)),
|
||||
_animation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,7 +399,10 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
return SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Transform.scale(scale: _scale.value, child: child),
|
||||
child: FadeTransition(
|
||||
opacity: _animation,
|
||||
child: ScaleTransition(scale: _animation, child: child),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
|
||||
@@ -262,11 +262,10 @@ class _PrimaryColorItemState extends ConsumerState<_PrimaryColorItem> {
|
||||
final vm4 = ref.watch(
|
||||
themeSettingProvider.select(
|
||||
(state) => VM4(
|
||||
a: state.primaryColor,
|
||||
b: state.primaryColors,
|
||||
c: state.schemeVariant,
|
||||
d:
|
||||
state.primaryColor == defaultPrimaryColor &&
|
||||
state.primaryColor,
|
||||
state.primaryColors,
|
||||
state.schemeVariant,
|
||||
state.primaryColor == defaultPrimaryColor &&
|
||||
intListEquality.equals(state.primaryColors, defaultPrimaryColors),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -79,7 +79,7 @@ class CommonCard extends StatelessWidget {
|
||||
this.type = CommonCardType.plain,
|
||||
this.onPressed,
|
||||
this.selectWidget,
|
||||
this.radius = 12,
|
||||
this.radius = 14,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.enterAnimated = false,
|
||||
@@ -177,7 +177,9 @@ class CommonCard extends StatelessWidget {
|
||||
style: ButtonStyle(
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius)),
|
||||
RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
iconColor: WidgetStatePropertyAll(context.colorScheme.primary),
|
||||
iconSize: WidgetStateProperty.all(20),
|
||||
|
||||
@@ -37,7 +37,7 @@ class ColorSchemeBox extends StatelessWidget {
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ClipRRect(
|
||||
child: ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
child: SizedBox(
|
||||
width: 72,
|
||||
@@ -47,22 +47,16 @@ class ColorSchemeBox extends StatelessWidget {
|
||||
children: [
|
||||
GridItem(
|
||||
mainAxisCellCount: 2,
|
||||
child: Container(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
child: Container(color: colorScheme.primary),
|
||||
),
|
||||
GridItem(
|
||||
mainAxisCellCount: 1,
|
||||
child: Container(
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
child: Container(color: colorScheme.secondary),
|
||||
),
|
||||
GridItem(
|
||||
mainAxisCellCount: 1,
|
||||
child: Container(
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
)
|
||||
child: Container(color: colorScheme.tertiary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -73,11 +67,8 @@ class ColorSchemeBox extends StatelessWidget {
|
||||
const Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
child: Icon(
|
||||
Icons.colorize,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
child: Icon(Icons.colorize, size: 20),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -112,9 +103,7 @@ class PrimaryColorBox extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
return Theme(
|
||||
data: themeData.copyWith(
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
data: themeData.copyWith(colorScheme: colorScheme),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class CommonTargetIcon extends StatelessWidget {
|
||||
@@ -60,7 +59,7 @@ class _ImageCacheWidgetState extends State<ImageCacheWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_imageFuture = DefaultCacheManager().getSingleFile(widget.src);
|
||||
_imageFuture = LocalImageCacheManager().getSingleFile(widget.src);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,30 +18,21 @@ class RadioDelegate<T> extends Delegate {
|
||||
final T value;
|
||||
final void Function()? onTab;
|
||||
|
||||
const RadioDelegate({
|
||||
required this.value,
|
||||
this.onTab,
|
||||
});
|
||||
const RadioDelegate({required this.value, this.onTab});
|
||||
}
|
||||
|
||||
class SwitchDelegate<T> extends Delegate {
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
const SwitchDelegate({
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
});
|
||||
const SwitchDelegate({required this.value, this.onChanged});
|
||||
}
|
||||
|
||||
class CheckboxDelegate<T> extends Delegate {
|
||||
final bool value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
|
||||
const CheckboxDelegate({
|
||||
this.value = false,
|
||||
this.onChanged,
|
||||
});
|
||||
const CheckboxDelegate({this.value = false, this.onChanged});
|
||||
}
|
||||
|
||||
class OpenDelegate extends Delegate {
|
||||
@@ -129,6 +120,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
final double? horizontalTitleGap;
|
||||
final TextStyle? titleTextStyle;
|
||||
final TextStyle? subtitleTextStyle;
|
||||
final double minVerticalPadding;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ListItem({
|
||||
@@ -143,6 +135,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.onTap,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : delegate = const Delegate();
|
||||
|
||||
@@ -158,6 +151,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -173,6 +167,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -188,6 +183,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -203,6 +199,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -217,9 +214,10 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
|
||||
const ListItem.switchItem({
|
||||
super.key,
|
||||
@@ -232,9 +230,10 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
|
||||
const ListItem.radio({
|
||||
super.key,
|
||||
@@ -247,9 +246,10 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : leading = null,
|
||||
onTap = null;
|
||||
}) : leading = null,
|
||||
onTap = null;
|
||||
|
||||
Widget _buildListTile({
|
||||
void Function()? onTap,
|
||||
@@ -264,7 +264,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
leading: leading ?? this.leading,
|
||||
horizontalTitleGap: horizontalTitleGap,
|
||||
title: title,
|
||||
minVerticalPadding: 12,
|
||||
minVerticalPadding: minVerticalPadding,
|
||||
subtitle: subtitle,
|
||||
titleAlignment: tileTitleAlignment,
|
||||
onTap: onTap,
|
||||
@@ -282,7 +282,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
closedBuilder: (_, action) {
|
||||
openAction() {
|
||||
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
||||
if (!isMobile || system.isDesktop) {
|
||||
if (!isMobile) {
|
||||
showExtend(
|
||||
context,
|
||||
props: ExtendProps(
|
||||
@@ -306,9 +306,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
action();
|
||||
}
|
||||
|
||||
return _buildListTile(
|
||||
onTap: openAction,
|
||||
);
|
||||
return _buildListTile(onTap: openAction);
|
||||
},
|
||||
openBuilder: (_, action) {
|
||||
return openDelegate.wrap
|
||||
@@ -422,9 +420,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return _buildListTile(
|
||||
onTap: onTap,
|
||||
);
|
||||
return _buildListTile(onTap: onTap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,13 +444,9 @@ class ListHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: padding ??
|
||||
const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 8,
|
||||
top: 24,
|
||||
bottom: 8,
|
||||
),
|
||||
padding:
|
||||
padding ??
|
||||
const EdgeInsets.only(left: 16, right: 8, top: 24, bottom: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -466,19 +458,18 @@ class ListHeader extends StatelessWidget {
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.opacity80,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.opacity80,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (subTitle != null)
|
||||
Text(
|
||||
subTitle!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -486,12 +477,7 @@ class ListHeader extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
...genActions(
|
||||
actions,
|
||||
space: space,
|
||||
),
|
||||
],
|
||||
children: [...genActions(actions, space: space)],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -506,18 +492,11 @@ List<Widget> generateSection({
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = separated
|
||||
? items.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
? items.separated(const Divider(height: 0))
|
||||
: items;
|
||||
return [
|
||||
if (items.isNotEmpty && title != null)
|
||||
ListHeader(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
ListHeader(title: title, actions: actions),
|
||||
...genItems,
|
||||
];
|
||||
}
|
||||
@@ -528,22 +507,26 @@ Widget generateSectionV2({
|
||||
List<Widget>? actions,
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = items
|
||||
.map<Widget>((item) {
|
||||
return ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CommonCard(
|
||||
type: CommonCardType.filled,
|
||||
radius: 0,
|
||||
child: item,
|
||||
),
|
||||
);
|
||||
})
|
||||
.separated(const Divider(height: 2, color: Colors.transparent));
|
||||
return Column(
|
||||
children: [
|
||||
if (items.isNotEmpty && title != null)
|
||||
ListHeader(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
CommonCard(
|
||||
radius: 18,
|
||||
type: CommonCardType.filled,
|
||||
child: Column(
|
||||
children: [
|
||||
...items,
|
||||
],
|
||||
),
|
||||
)
|
||||
ListHeader(title: title, actions: actions),
|
||||
ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Column(children: [...genItems]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -555,18 +538,10 @@ List<Widget> generateInfoSection({
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = separated
|
||||
? items.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
? items.separated(const Divider(height: 0))
|
||||
: items;
|
||||
return [
|
||||
if (items.isNotEmpty)
|
||||
InfoHeader(
|
||||
info: info,
|
||||
actions: actions,
|
||||
),
|
||||
if (items.isNotEmpty) InfoHeader(info: info, actions: actions),
|
||||
...genItems,
|
||||
];
|
||||
}
|
||||
@@ -575,8 +550,6 @@ Widget generateListView(List<Widget> items) {
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) => items[index],
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 16,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
typedef CloseContainerActionCallback<S> = void Function({S? returnValue});
|
||||
typedef OpenContainerBuilder<S> = Widget Function(
|
||||
BuildContext context,
|
||||
CloseContainerActionCallback<S> action,
|
||||
);
|
||||
typedef CloseContainerBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
VoidCallback action,
|
||||
);
|
||||
typedef OpenContainerBuilder<S> =
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
CloseContainerActionCallback<S> action,
|
||||
);
|
||||
typedef CloseContainerBuilder =
|
||||
Widget Function(BuildContext context, VoidCallback action);
|
||||
|
||||
enum ContainerTransitionType {
|
||||
fade,
|
||||
fadeThrough,
|
||||
}
|
||||
enum ContainerTransitionType { fade, fadeThrough }
|
||||
|
||||
typedef ClosedCallback<S> = void Function(S data);
|
||||
|
||||
@@ -56,20 +52,23 @@ class _OpenContainerState<T> extends State<OpenContainer<T?>> {
|
||||
Future<void> openContainer() async {
|
||||
final Color middleColor =
|
||||
widget.middleColor ?? Theme.of(context).canvasColor;
|
||||
final T? data = await Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.useRootNavigator,
|
||||
).push(_OpenContainerRoute<T>(
|
||||
middleColor: middleColor,
|
||||
closedBuilder: widget.closedBuilder,
|
||||
openBuilder: widget.openBuilder,
|
||||
hideableKey: _hideableKey,
|
||||
closedBuilderKey: _closedBuilderKey,
|
||||
transitionDuration: widget.transitionDuration,
|
||||
transitionType: widget.transitionType,
|
||||
useRootNavigator: widget.useRootNavigator,
|
||||
routeSettings: widget.routeSettings,
|
||||
));
|
||||
final T? data =
|
||||
await Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.useRootNavigator,
|
||||
).push(
|
||||
_OpenContainerRoute<T>(
|
||||
middleColor: middleColor,
|
||||
closedBuilder: widget.closedBuilder,
|
||||
openBuilder: widget.openBuilder,
|
||||
hideableKey: _hideableKey,
|
||||
closedBuilderKey: _closedBuilderKey,
|
||||
transitionDuration: widget.transitionDuration,
|
||||
transitionType: widget.transitionType,
|
||||
useRootNavigator: widget.useRootNavigator,
|
||||
routeSettings: widget.routeSettings,
|
||||
),
|
||||
);
|
||||
if (widget.onClosed != null) {
|
||||
widget.onClosed!(data);
|
||||
}
|
||||
@@ -97,10 +96,7 @@ class _OpenContainerState<T> extends State<OpenContainer<T?>> {
|
||||
}
|
||||
|
||||
class _Hideable extends StatefulWidget {
|
||||
const _Hideable({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
const _Hideable({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@@ -161,9 +157,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
required this.transitionType,
|
||||
required this.useRootNavigator,
|
||||
required RouteSettings? routeSettings,
|
||||
}) : _closedOpacityTween = _getClosedOpacityTween(transitionType),
|
||||
_openOpacityTween = _getOpenOpacityTween(transitionType),
|
||||
super(settings: routeSettings);
|
||||
}) : _closedOpacityTween = _getClosedOpacityTween(transitionType),
|
||||
_openOpacityTween = _getOpenOpacityTween(transitionType),
|
||||
super(settings: routeSettings);
|
||||
|
||||
static _FlippableTweenSequence<Color?> _getColorTween({
|
||||
required ContainerTransitionType transitionType,
|
||||
@@ -173,99 +169,89 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
}) {
|
||||
switch (transitionType) {
|
||||
case ContainerTransitionType.fade:
|
||||
return _FlippableTweenSequence<Color?>(
|
||||
<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(closedColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: openColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(openColor),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<Color?>(<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(closedColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: openColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(openColor),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
]);
|
||||
case ContainerTransitionType.fadeThrough:
|
||||
return _FlippableTweenSequence<Color?>(
|
||||
<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: middleColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: middleColor, end: openColor),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<Color?>(<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: middleColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: middleColor, end: openColor),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static _FlippableTweenSequence<double> _getClosedOpacityTween(
|
||||
ContainerTransitionType transitionType) {
|
||||
ContainerTransitionType transitionType,
|
||||
) {
|
||||
switch (transitionType) {
|
||||
case ContainerTransitionType.fade:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 1,
|
||||
),
|
||||
]);
|
||||
case ContainerTransitionType.fadeThrough:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 1.0, end: 0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 1.0, end: 0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static _FlippableTweenSequence<double> _getOpenOpacityTween(
|
||||
ContainerTransitionType transitionType) {
|
||||
ContainerTransitionType transitionType,
|
||||
) {
|
||||
switch (transitionType) {
|
||||
case ContainerTransitionType.fade:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
]);
|
||||
case ContainerTransitionType.fadeThrough:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,8 +311,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
@override
|
||||
void dispose() {
|
||||
if (hideableKey.currentState?.isVisible == false) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration d) => _toggleHideable(hide: false));
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(Duration d) => _toggleHideable(hide: false),
|
||||
);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@@ -343,10 +330,12 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
required BuildContext navigatorContext,
|
||||
bool delayForSourceRoute = false,
|
||||
}) {
|
||||
final RenderBox navigator = Navigator.of(
|
||||
navigatorContext,
|
||||
rootNavigator: useRootNavigator,
|
||||
).context.findRenderObject()! as RenderBox;
|
||||
final RenderBox navigator =
|
||||
Navigator.of(
|
||||
navigatorContext,
|
||||
rootNavigator: useRootNavigator,
|
||||
).context.findRenderObject()!
|
||||
as RenderBox;
|
||||
final Size navSize = _getSize(navigator);
|
||||
_rectTween.end = Offset.zero & navSize;
|
||||
|
||||
@@ -359,8 +348,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
}
|
||||
|
||||
if (delayForSourceRoute) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback(takeMeasurementsInSourceRoute);
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
takeMeasurementsInSourceRoute,
|
||||
);
|
||||
} else {
|
||||
takeMeasurementsInSourceRoute();
|
||||
}
|
||||
@@ -451,8 +441,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
final Animation<double> curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
reverseCurve:
|
||||
_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
|
||||
reverseCurve: _transitionWasInterrupted
|
||||
? null
|
||||
: Curves.fastOutSlowIn.flipped,
|
||||
);
|
||||
TweenSequence<Color?>? colorTween;
|
||||
TweenSequence<double>? closedOpacityTween, openOpacityTween;
|
||||
@@ -508,8 +499,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
child: (hideableKey.currentState?.isInTree ?? false)
|
||||
? null
|
||||
: FadeTransition(
|
||||
opacity:
|
||||
closedOpacityTween!.animate(animation),
|
||||
opacity: closedOpacityTween!.animate(
|
||||
animation,
|
||||
),
|
||||
child: Builder(
|
||||
key: closedBuilderKey,
|
||||
builder: (BuildContext context) {
|
||||
@@ -521,22 +513,18 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Open child fading in.
|
||||
FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
OverflowBox(
|
||||
maxWidth: _rectTween.end!.width,
|
||||
maxHeight: _rectTween.end!.height,
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: _rectTween.end!.width,
|
||||
height: _rectTween.end!.height,
|
||||
child: FadeTransition(
|
||||
opacity: openOpacityTween!.animate(animation),
|
||||
child: Builder(
|
||||
key: _openBuilderKey,
|
||||
builder: (BuildContext context) {
|
||||
return openBuilder(context, closeContainer);
|
||||
},
|
||||
),
|
||||
child: FadeTransition(
|
||||
opacity: openOpacityTween!.animate(animation),
|
||||
child: Builder(
|
||||
key: _openBuilderKey,
|
||||
builder: (BuildContext context) {
|
||||
return openBuilder(context, closeContainer);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -578,10 +566,12 @@ class _FlippableTweenSequence<T> extends TweenSequence<T> {
|
||||
if (_flipped == null) {
|
||||
final List<TweenSequenceItem<T>> newItems = <TweenSequenceItem<T>>[];
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
newItems.add(TweenSequenceItem<T>(
|
||||
tween: _items[i].tween,
|
||||
weight: _items[_items.length - 1 - i].weight,
|
||||
));
|
||||
newItems.add(
|
||||
TweenSequenceItem<T>(
|
||||
tween: _items[i].tween,
|
||||
weight: _items[_items.length - 1 - i].weight,
|
||||
),
|
||||
);
|
||||
}
|
||||
_flipped = _FlippableTweenSequence<T>(newItems);
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ class _ShadePainter extends CustomPainter {
|
||||
effectiveSquareRadius * 2,
|
||||
effectiveSquareRadius * 2,
|
||||
);
|
||||
final RRect rRect = RRect.fromRectAndRadius(
|
||||
final RSuperellipse rSuperellipse = RSuperellipse.fromRectAndRadius(
|
||||
rectBox,
|
||||
Radius.circular(trackBorderRadius),
|
||||
);
|
||||
@@ -254,8 +254,8 @@ class _ShadePainter extends CustomPainter {
|
||||
HSVColor.fromAHSV(1, colorHue, 1, 1).toColor(),
|
||||
],
|
||||
).createShader(rectBox);
|
||||
canvas.drawRRect(
|
||||
rRect,
|
||||
canvas.drawRSuperellipse(
|
||||
rSuperellipse,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..shader = horizontal,
|
||||
@@ -266,8 +266,8 @@ class _ShadePainter extends CustomPainter {
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[Colors.transparent, Colors.black],
|
||||
).createShader(rectBox);
|
||||
canvas.drawRRect(
|
||||
rRect,
|
||||
canvas.drawRSuperellipse(
|
||||
rSuperellipse,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..shader = vertical,
|
||||
|
||||
@@ -38,10 +38,9 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
Widget child,
|
||||
) {
|
||||
final align = Alignment.topRight;
|
||||
final animationValue = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeIn,
|
||||
).value;
|
||||
final curveAnimation = animation
|
||||
.drive(Tween(begin: 0.0, end: 1.0))
|
||||
.drive(CurveTween(curve: Curves.easeOutBack));
|
||||
return SafeArea(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: offsetNotifier,
|
||||
@@ -58,15 +57,17 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (_, Widget? child) {
|
||||
return Opacity(
|
||||
opacity: 0.1 + 0.9 * animationValue,
|
||||
child: Transform.scale(
|
||||
builder: (_, child) {
|
||||
return FadeTransition(
|
||||
opacity: curveAnimation,
|
||||
child: ScaleTransition(
|
||||
alignment: align,
|
||||
scale: 0.7 + 0.3 * animationValue,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -10) * (1 - animationValue),
|
||||
child: child!,
|
||||
scale: curveAnimation,
|
||||
child: SlideTransition(
|
||||
position: curveAnimation.drive(
|
||||
Tween(begin: const Offset(0, -0.02), end: Offset.zero),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -78,7 +79,7 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 150);
|
||||
Duration get transitionDuration => const Duration(milliseconds: 250);
|
||||
}
|
||||
|
||||
class PopupController extends ValueNotifier<bool> {
|
||||
@@ -270,8 +271,8 @@ class CommonPopupMenu extends StatelessWidget {
|
||||
elevation: 12,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -18,7 +18,6 @@ class CommonScaffold extends StatefulWidget {
|
||||
final Widget body;
|
||||
final Color? backgroundColor;
|
||||
final String? title;
|
||||
final Widget? leading;
|
||||
final List<Widget>? actions;
|
||||
final bool? centerTitle;
|
||||
final Widget? floatingActionButton;
|
||||
@@ -31,7 +30,6 @@ class CommonScaffold extends StatefulWidget {
|
||||
this.appBar,
|
||||
required this.body,
|
||||
this.backgroundColor,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.centerTitle,
|
||||
@@ -163,7 +161,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
_keywordsNotifier.value = keywords;
|
||||
}
|
||||
|
||||
Widget? _buildLeading() {
|
||||
Widget? _buildLeading(VoidCallback? backAction) {
|
||||
if (_isEdit) {
|
||||
return IconButton(
|
||||
onPressed: _appBarState.value.editState?.onExit,
|
||||
@@ -176,7 +174,16 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
icon: Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
return widget.leading;
|
||||
return backAction != null
|
||||
? BackButton(
|
||||
onPressed: () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
backAction();
|
||||
},
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
Widget _buildTitle(AppBarSearchState? startState) {
|
||||
@@ -251,7 +258,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
PreferredSizeWidget _buildAppBar(VoidCallback? backAction) {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Stack(
|
||||
@@ -263,8 +270,11 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
builder: (_, state, _) {
|
||||
return _buildAppBarWrap(
|
||||
AppBar(
|
||||
automaticallyImplyLeading: backAction != null
|
||||
? false
|
||||
: true,
|
||||
centerTitle: widget.centerTitle ?? false,
|
||||
leading: _buildLeading(),
|
||||
leading: _buildLeading(backAction),
|
||||
title: _buildTitle(state.searchState),
|
||||
actions: _buildActions(
|
||||
state.searchState != null,
|
||||
@@ -285,6 +295,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(widget.appBar != null || widget.title != null);
|
||||
final backActionProvider = CommonScaffoldBackActionProvider.of(context);
|
||||
final body = SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -327,7 +338,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(),
|
||||
appBar: _buildAppBar(backActionProvider?.backAction),
|
||||
body: body,
|
||||
resizeToAvoidBottomInset: true,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
@@ -349,3 +360,23 @@ List<Widget> genActions(List<Widget> actions, {double? space}) {
|
||||
SizedBox(width: 8),
|
||||
];
|
||||
}
|
||||
|
||||
class CommonScaffoldBackActionProvider extends InheritedWidget {
|
||||
final VoidCallback? backAction;
|
||||
|
||||
const CommonScaffoldBackActionProvider({
|
||||
super.key,
|
||||
required this.backAction,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static CommonScaffoldBackActionProvider? of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<CommonScaffoldBackActionProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(CommonScaffoldBackActionProvider oldWidget) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@ class ExtendProps {
|
||||
});
|
||||
}
|
||||
|
||||
enum SheetType {
|
||||
page,
|
||||
bottomSheet,
|
||||
sideSheet,
|
||||
}
|
||||
enum SheetType { page, bottomSheet, sideSheet }
|
||||
|
||||
typedef SheetBuilder = Widget Function(BuildContext context, SheetType type);
|
||||
|
||||
@@ -55,28 +51,24 @@ Future<T?> showSheet<T>({
|
||||
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
||||
return switch (isMobile) {
|
||||
true => showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
child: builder(context, SheetType.bottomSheet),
|
||||
);
|
||||
},
|
||||
showDragHandle: false,
|
||||
useSafeArea: props.useSafeArea,
|
||||
),
|
||||
context: context,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
builder: (_) {
|
||||
return SafeArea(child: builder(context, SheetType.bottomSheet));
|
||||
},
|
||||
showDragHandle: false,
|
||||
useSafeArea: props.useSafeArea,
|
||||
),
|
||||
false => showModalSideSheet<T>(
|
||||
useSafeArea: props.useSafeArea,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
context: context,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: props.maxWidth ?? 360,
|
||||
),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (_) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
useSafeArea: props.useSafeArea,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
context: context,
|
||||
constraints: BoxConstraints(maxWidth: props.maxWidth ?? 360),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (_) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,21 +79,16 @@ Future<T?> showExtend<T>(
|
||||
}) {
|
||||
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
||||
return switch (isMobile || props.forceFull) {
|
||||
true => BaseNavigator.push(
|
||||
context,
|
||||
builder(context, SheetType.page),
|
||||
),
|
||||
true => BaseNavigator.push(context, builder(context, SheetType.page)),
|
||||
false => showModalSideSheet<T>(
|
||||
useSafeArea: props.useSafeArea,
|
||||
context: context,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: props.maxWidth ?? 360,
|
||||
),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (context) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
useSafeArea: props.useSafeArea,
|
||||
context: context,
|
||||
constraints: BoxConstraints(maxWidth: props.maxWidth ?? 360),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (context) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,13 +121,11 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
|
||||
automaticallyImplyLeading: bottomSheet
|
||||
? false
|
||||
: widget.actions.isEmpty && sideSheet
|
||||
? false
|
||||
: true,
|
||||
? false
|
||||
: true,
|
||||
centerTitle: bottomSheet,
|
||||
backgroundColor: backgroundColor,
|
||||
title: Text(
|
||||
widget.title,
|
||||
),
|
||||
title: Text(widget.title),
|
||||
actions: genActions([
|
||||
if (widget.actions.isEmpty && sideSheet) CloseButton(),
|
||||
...widget.actions,
|
||||
@@ -150,9 +135,11 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
|
||||
final handleSize = Size(32, 4);
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -163,17 +150,16 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
|
||||
alignment: Alignment.center,
|
||||
height: handleSize.height,
|
||||
width: handleSize.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(handleSize.height / 2),
|
||||
decoration: ShapeDecoration(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(handleSize.height / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
appBar,
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: widget.body,
|
||||
)
|
||||
Flexible(flex: 1, child: widget.body),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user