Compare commits
1 Commits
v0.8.88-pr
...
v0.8.88
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed7868282a |
3
.github/workflows/build.yaml
vendored
3
.github/workflows/build.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- platform: android
|
||||
os: ubuntu-latest
|
||||
- platform: windows
|
||||
os: windows-latest
|
||||
os: Windows-2022
|
||||
arch: amd64
|
||||
- platform: linux
|
||||
os: ubuntu-22.04
|
||||
@@ -52,6 +52,7 @@ jobs:
|
||||
if: startsWith(matrix.platform,'android')
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
|
||||
echo "${{ secrets.SERVICE_JSON }}" | base64 --decode > android/app/google-services.json
|
||||
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
|
||||
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
|
||||
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
|
||||
|
||||
@@ -5,6 +5,8 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
}
|
||||
|
||||
val localPropertiesFile = rootProject.file("local.properties")
|
||||
@@ -18,10 +20,9 @@ val mStoreFile: File = file("keystore.jks")
|
||||
val mStorePassword: String? = localProperties.getProperty("storePassword")
|
||||
val mKeyAlias: String? = localProperties.getProperty("keyAlias")
|
||||
val mKeyPassword: String? = localProperties.getProperty("keyPassword")
|
||||
val isRelease = mStoreFile.exists()
|
||||
&& mStorePassword != null
|
||||
&& mKeyAlias != null
|
||||
&& mKeyPassword != null
|
||||
val isRelease =
|
||||
mStoreFile.exists() && mStorePassword != null && mKeyAlias != null && mKeyPassword != null
|
||||
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash"
|
||||
@@ -29,6 +30,7 @@ android {
|
||||
ndkVersion = libs.versions.ndkVersion.get()
|
||||
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
@@ -53,6 +55,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
@@ -69,8 +77,7 @@ android {
|
||||
}
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -86,6 +93,7 @@ flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation(project(":service"))
|
||||
implementation(project(":common"))
|
||||
@@ -94,4 +102,7 @@ dependencies {
|
||||
implementation(libs.smali.dexlib2) {
|
||||
exclude(group = "com.google.guava", module = "guava")
|
||||
}
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.crashlytics.ndk)
|
||||
implementation(libs.firebase.analytics)
|
||||
}
|
||||
46
android/app/google-services.json
Normal file
46
android/app/google-services.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "000000000000",
|
||||
"project_id": "dev"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
|
||||
"android_client_info": {
|
||||
"package_name": "com.follow.clash"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "0"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
|
||||
"android_client_info": {
|
||||
"package_name": "com.follow.clash.debug"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "0"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
<service
|
||||
android:name=".TileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label"
|
||||
tools:targetApi="24" />
|
||||
tools:replace="android:label" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
@@ -24,28 +20,23 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:extractNativeLibs="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
@@ -112,35 +103,11 @@
|
||||
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>
|
||||
|
||||
|
||||
<provider
|
||||
android:name=".FilesProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:process=":background">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -4,31 +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 -> {
|
||||
BroadcastAction.SERVICE_DESTROYED.action -> {
|
||||
GlobalState.log("Receiver service destroyed")
|
||||
State.handleStopServiceAction()
|
||||
}
|
||||
|
||||
BroadcastAction.TOGGLE.action -> {
|
||||
launch {
|
||||
State.handleToggleAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,15 @@ suspend fun PackageManager.getPackageIconPath(packageName: String): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val cacheDir = GlobalState.application.cacheDir
|
||||
val iconDir = File(cacheDir, "icons").apply { mkdirs() }
|
||||
val pkgInfo = getPackageInfo(packageName, 0)
|
||||
val lastUpdateTime = pkgInfo.lastUpdateTime
|
||||
val iconFile = File(iconDir, "${packageName}_${lastUpdateTime}.webp")
|
||||
if (iconFile.exists() && !isExpired(iconFile)) {
|
||||
return@withContext iconFile.absolutePath
|
||||
}
|
||||
iconDir.listFiles()?.forEach { file ->
|
||||
if (file.name.startsWith(packageName + "_")) file.delete()
|
||||
}
|
||||
return@withContext try {
|
||||
val pkgInfo = getPackageInfo(packageName, 0)
|
||||
val lastUpdateTime = pkgInfo.lastUpdateTime
|
||||
val iconFile = File(iconDir, "${packageName}_${lastUpdateTime}.webp")
|
||||
if (iconFile.exists() && !isExpired(iconFile)) {
|
||||
return@withContext iconFile.absolutePath
|
||||
}
|
||||
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 -> {
|
||||
@@ -97,17 +98,13 @@ inline fun <reified T : FlutterPlugin> FlutterEngine.plugin(): T? {
|
||||
return plugins.get(T::class.java) as T?
|
||||
}
|
||||
|
||||
|
||||
fun <T> MethodChannel.invokeMethodOnMainThread(
|
||||
method: String,
|
||||
arguments: Any? = null,
|
||||
callback: ((Result<T>) -> Unit)? = null
|
||||
method: String, arguments: Any? = null, callback: ((Result<T>) -> Unit)? = null
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invokeMethod(method, arguments, object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callback?.invoke(Result.success(result as T))
|
||||
@Suppress("UNCHECKED_CAST") callback?.invoke(Result.success(result as T))
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -1,78 +1,143 @@
|
||||
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.ICallbackInterface
|
||||
import com.follow.clash.service.IMessageInterface
|
||||
import com.follow.clash.service.IEventInterface
|
||||
import com.follow.clash.service.IRemoteInterface
|
||||
import com.follow.clash.service.IResultInterface
|
||||
import com.follow.clash.service.RemoteService
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
object Service {
|
||||
private val delegate by lazy {
|
||||
ServiceDelegate<IRemoteInterface>(
|
||||
RemoteService::class.intent, ::handleOnServiceCrash
|
||||
RemoteService::class.intent, ::handleServiceDisconnected
|
||||
) {
|
||||
IRemoteInterface.Stub.asInterface(it)
|
||||
}
|
||||
}
|
||||
|
||||
var onServiceCrash: (() -> Unit)? = null
|
||||
var onServiceDisconnected: ((String) -> Unit)? = null
|
||||
|
||||
private fun handleOnServiceCrash() {
|
||||
bindingState.set(false)
|
||||
onServiceCrash?.let {
|
||||
it()
|
||||
private fun handleServiceDisconnected(message: String) {
|
||||
onServiceDisconnected?.let {
|
||||
it(message)
|
||||
}
|
||||
}
|
||||
|
||||
private val bindingState = AtomicBoolean(false)
|
||||
|
||||
fun bind() {
|
||||
if (bindingState.compareAndSet(false, true)) {
|
||||
delegate.bind()
|
||||
delegate.bind()
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
delegate.unbind()
|
||||
}
|
||||
|
||||
suspend fun invokeAction(data: String, cb: (result: String) -> Unit): Result<Unit> {
|
||||
val res = mutableListOf<ByteArray>()
|
||||
return delegate.useService {
|
||||
it.invokeAction(
|
||||
data, object : ICallbackInterface.Stub() {
|
||||
override fun onResult(result: ByteArray?, isSuccess: Boolean) {
|
||||
res.add(result ?: byteArrayOf())
|
||||
if (isSuccess) {
|
||||
cb(res.formatString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invokeAction(
|
||||
data: String, cb: (result: ByteArray?, isSuccess: Boolean) -> Unit
|
||||
) {
|
||||
delegate.useService {
|
||||
it.invokeAction(data, object : ICallbackInterface.Stub() {
|
||||
override fun onResult(result: ByteArray?, isSuccess: Boolean) {
|
||||
cb(result, isSuccess)
|
||||
suspend fun setEventListener(
|
||||
cb: ((result: String?) -> Unit)?
|
||||
): Result<Unit> {
|
||||
val results = HashMap<String, MutableList<ByteArray>>()
|
||||
return delegate.useService {
|
||||
it.setEventListener(
|
||||
when (cb != null) {
|
||||
true -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false -> null
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateNotificationParams(
|
||||
params: NotificationParams
|
||||
) {
|
||||
delegate.useService {
|
||||
): Result<Unit> {
|
||||
return delegate.useService {
|
||||
it.updateNotificationParams(params)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setMessageCallback(
|
||||
cb: (result: String?) -> Unit
|
||||
) {
|
||||
delegate.useService {
|
||||
it.setMessageCallback(object : IMessageInterface.Stub() {
|
||||
override fun onResult(result: String?) {
|
||||
cb(result)
|
||||
}
|
||||
})
|
||||
suspend fun setCrashlytics(
|
||||
enable: Boolean
|
||||
): Result<Unit> {
|
||||
return delegate.useService {
|
||||
it.setCrashlytics(enable)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startService(options: VpnOptions, inApp: Boolean) {
|
||||
delegate.useService { it.startService(options, inApp) }
|
||||
private suspend fun awaitIResultInterface(
|
||||
block: (IResultInterface) -> Unit
|
||||
): Long = suspendCancellableCoroutine { continuation ->
|
||||
val callback = object : IResultInterface.Stub() {
|
||||
override fun onResult(time: Long) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
block(callback)
|
||||
} catch (e: Exception) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopService() {
|
||||
delegate.useService { it.stopService() }
|
||||
|
||||
suspend fun startService(options: VpnOptions, runTime: Long): Long {
|
||||
return delegate.useService {
|
||||
awaitIResultInterface { callback ->
|
||||
it.startService(options, runTime, callback)
|
||||
}
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
suspend fun stopService(): Long {
|
||||
return delegate.useService {
|
||||
awaitIResultInterface { callback ->
|
||||
it.stopService(callback)
|
||||
}
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
suspend fun getRunTime(): Long {
|
||||
return delegate.useService {
|
||||
it.runTime
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ object State {
|
||||
var runTime: Long = 0
|
||||
|
||||
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
|
||||
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
var serviceFlutterEngine: FlutterEngine? = null
|
||||
|
||||
@@ -51,6 +52,18 @@ object State {
|
||||
action?.invoke()
|
||||
}
|
||||
|
||||
suspend fun handleSyncState() {
|
||||
runLock.withLock {
|
||||
Service.bind()
|
||||
runTime = Service.getRunTime()
|
||||
val runState = when (runTime == 0L) {
|
||||
true -> RunState.STOP
|
||||
false -> RunState.START
|
||||
}
|
||||
runStateFlow.tryEmit(runState)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleStartServiceAction() {
|
||||
tilePlugin?.handleStart()
|
||||
if (flutterEngine != null) {
|
||||
@@ -90,6 +103,9 @@ object State {
|
||||
|
||||
suspend fun startServiceWithEngine() {
|
||||
runLock.withLock {
|
||||
if (serviceFlutterEngine != null || runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
serviceFlutterEngine = FlutterEngine(GlobalState.application)
|
||||
serviceFlutterEngine?.plugins?.add(ServicePlugin())
|
||||
@@ -100,7 +116,6 @@ object State {
|
||||
)
|
||||
serviceFlutterEngine?.dartExecutor?.executeDartEntrypoint(dartEntrypoint)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +134,8 @@ object State {
|
||||
return@launch
|
||||
}
|
||||
appPlugin?.prepare(options.enable) {
|
||||
Service.startService(options, true)
|
||||
runTime = Service.startService(options, runTime)
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
runTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,14 +149,13 @@ object State {
|
||||
return@launch
|
||||
}
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
Service.stopService()
|
||||
runTime = Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
runTime = 0
|
||||
}
|
||||
destroyServiceEngine()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class TileService : TileService() {
|
||||
scope?.cancel()
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
scope?.launch {
|
||||
State.handleSyncState()
|
||||
State.runStateFlow.collect {
|
||||
updateTile(it)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.follow.clash.models
|
||||
|
||||
|
||||
data class AppState(
|
||||
val currentProfileName: String,
|
||||
val stopText: String,
|
||||
val onlyStatisticsProxy: Boolean,
|
||||
val crashlytics: Boolean = true,
|
||||
val currentProfileName: String = "FlClash",
|
||||
val stopText: String = "Stop",
|
||||
val onlyStatisticsProxy: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -58,8 +58,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
|
||||
private var requestNotificationCallback: (() -> Unit)? = null
|
||||
|
||||
private val iconMap = mutableMapOf<String, String?>()
|
||||
|
||||
private val packages = mutableListOf<Package>()
|
||||
|
||||
private val skipPrefixList = listOf(
|
||||
@@ -172,8 +170,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
result.success("")
|
||||
return@launch
|
||||
}
|
||||
val path =
|
||||
GlobalState.application.packageManager.getPackageIconPath(packageName)
|
||||
val path = GlobalState.application.packageManager.getPackageIconPath(packageName)
|
||||
result.success(path)
|
||||
}
|
||||
}
|
||||
@@ -223,13 +220,12 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
|
||||
?.filter {
|
||||
it.packageName != GlobalState.application.packageName || it.packageName == "android"
|
||||
|
||||
it.packageName != GlobalState.application.packageName && it.packageName != "android"
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) != 0,
|
||||
lastUpdateTime = it.lastUpdateTime,
|
||||
internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.follow.clash.Service
|
||||
import com.follow.clash.State
|
||||
import com.follow.clash.awaitResult
|
||||
import com.follow.clash.common.Components
|
||||
import com.follow.clash.common.formatString
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.invokeMethodOnMainThread
|
||||
import com.follow.clash.models.AppState
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
@@ -38,7 +38,11 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"init" -> {
|
||||
handleInit(result)
|
||||
handleInit(call, result)
|
||||
}
|
||||
|
||||
"shutdown" -> {
|
||||
handleShutdown(result)
|
||||
}
|
||||
|
||||
"invokeAction" -> {
|
||||
@@ -69,16 +73,17 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private fun handleInvokeAction(call: MethodCall, result: MethodChannel.Result) {
|
||||
launch {
|
||||
val data = call.arguments<String>()!!
|
||||
val res = mutableListOf<ByteArray>()
|
||||
Service.invokeAction(data) { byteArray, isSuccess ->
|
||||
res.add(byteArray ?: byteArrayOf())
|
||||
if (isSuccess) {
|
||||
result.success(res.formatString())
|
||||
}
|
||||
Service.invokeAction(data) {
|
||||
result.success(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleShutdown(result: MethodChannel.Result) {
|
||||
Service.unbind()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun handleStart(result: MethodChannel.Result) {
|
||||
State.handleStartService()
|
||||
result.success(true)
|
||||
@@ -104,15 +109,16 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
}
|
||||
|
||||
private fun onServiceCrash() {
|
||||
private fun onServiceDisconnected(message: String) {
|
||||
State.runStateFlow.tryEmit(RunState.STOP)
|
||||
flutterMethodChannel.invokeMethodOnMainThread<Any>("crash", null)
|
||||
flutterMethodChannel.invokeMethodOnMainThread<Any>("crash", message)
|
||||
}
|
||||
|
||||
private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) {
|
||||
val data = call.arguments<String>()!!
|
||||
val params = Gson().fromJson(data, AppState::class.java)
|
||||
GlobalState.setCrashlytics(params.crashlytics)
|
||||
launch {
|
||||
val data = call.arguments<String>()!!
|
||||
val params = Gson().fromJson(data, AppState::class.java)
|
||||
Service.updateNotificationParams(
|
||||
NotificationParams(
|
||||
title = params.currentProfileName,
|
||||
@@ -120,22 +126,35 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
onlyStatisticsProxy = params.onlyStatisticsProxy
|
||||
)
|
||||
)
|
||||
result.success(true)
|
||||
Service.setCrashlytics(params.crashlytics)
|
||||
result.success("")
|
||||
}
|
||||
}
|
||||
|
||||
fun handleInit(result: MethodChannel.Result) {
|
||||
fun handleInit(call: MethodCall, result: MethodChannel.Result) {
|
||||
Service.bind()
|
||||
launch {
|
||||
Service.setMessageCallback {
|
||||
handleSendEvent(it)
|
||||
val needSetEventListener = call.arguments<Boolean>() ?: false
|
||||
when (needSetEventListener) {
|
||||
true -> Service.setEventListener {
|
||||
handleSendEvent(it)
|
||||
}
|
||||
|
||||
false -> Service.setEventListener(null)
|
||||
}.onSuccess {
|
||||
result.success("")
|
||||
}.onFailure {
|
||||
result.success(it.message)
|
||||
}
|
||||
result.success(true)
|
||||
|
||||
}
|
||||
Service.onServiceCrash = ::onServiceCrash
|
||||
Service.onServiceDisconnected = ::onServiceDisconnected
|
||||
}
|
||||
|
||||
private fun handleGetRunTime(result: MethodChannel.Result) {
|
||||
return result.success(State.runTime)
|
||||
launch {
|
||||
State.handleSyncState()
|
||||
result.success(State.runTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.gson)
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.crashlytics.ndk)
|
||||
implementation(libs.firebase.analytics)
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<uses-permission android:name="${applicationId}.permission.RECEIVE_BROADCASTS" />
|
||||
</manifest>
|
||||
@@ -10,9 +10,8 @@ enum class QuickAction {
|
||||
}
|
||||
|
||||
enum class BroadcastAction {
|
||||
START,
|
||||
STOP,
|
||||
TOGGLE,
|
||||
SERVICE_CREATED,
|
||||
SERVICE_DESTROYED,
|
||||
}
|
||||
|
||||
enum class AccessControlMode {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -13,14 +14,21 @@ import android.content.Context.RECEIVER_NOT_EXPORTED
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -37,19 +45,23 @@ val KClass<*>.intent: Intent
|
||||
|
||||
fun Service.startForegroundCompat(id: Int, notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
startForeground(id, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
val ComponentName.intent: Intent
|
||||
get() = Intent().apply {
|
||||
setComponent(this@intent)
|
||||
setPackage(GlobalState.packageName)
|
||||
}
|
||||
|
||||
val QuickAction.action: String
|
||||
get() = "${GlobalState.application.packageName}.action.${this.name}"
|
||||
|
||||
val QuickAction.quickIntent: Intent
|
||||
get() = Intent().apply {
|
||||
setComponent(Components.TEMP_ACTIVITY)
|
||||
setPackage(GlobalState.packageName)
|
||||
get() = Components.TEMP_ACTIVITY.intent.apply {
|
||||
action = this@quickIntent.action
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
}
|
||||
@@ -57,10 +69,18 @@ val QuickAction.quickIntent: Intent
|
||||
val BroadcastAction.action: String
|
||||
get() = "${GlobalState.application.packageName}.intent.action.${this.name}"
|
||||
|
||||
val Context.processName: String?
|
||||
get() {
|
||||
val pid = android.os.Process.myPid()
|
||||
val activityManager = getSystemService<ActivityManager>()
|
||||
activityManager?.runningAppProcesses?.find { it.pid == pid }?.let {
|
||||
return it.processName
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val BroadcastAction.quickIntent: Intent
|
||||
get() = Intent().apply {
|
||||
setComponent(Components.BROADCAST_RECEIVER)
|
||||
setPackage(GlobalState.packageName)
|
||||
get() = Components.BROADCAST_RECEIVER.intent.apply {
|
||||
action = this@quickIntent.action
|
||||
}
|
||||
|
||||
@@ -126,62 +146,55 @@ fun Context.receiveBroadcastFlow(
|
||||
}
|
||||
|
||||
|
||||
sealed class BindServiceEvent<out T : IBinder> {
|
||||
data class Connected<T : IBinder>(val binder: T) : BindServiceEvent<T>()
|
||||
object Disconnected : BindServiceEvent<Nothing>()
|
||||
object Crashed : BindServiceEvent<Nothing>()
|
||||
}
|
||||
|
||||
inline fun <reified T : IBinder> Context.bindServiceFlow(
|
||||
intent: Intent,
|
||||
flags: Int = Context.BIND_AUTO_CREATE,
|
||||
): Flow<BindServiceEvent<T>> = callbackFlow {
|
||||
var currentBinder: IBinder? = null
|
||||
val deathRecipient = IBinder.DeathRecipient {
|
||||
trySend(BindServiceEvent.Crashed)
|
||||
}
|
||||
|
||||
maxRetries: Int = 10,
|
||||
retryDelayMillis: Long = 200L
|
||||
): Flow<Pair<IBinder?, String>> = callbackFlow {
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
if (binder != null) {
|
||||
try {
|
||||
binder.linkToDeath(deathRecipient, 0)
|
||||
currentBinder = binder
|
||||
@Suppress("UNCHECKED_CAST") val casted = binder as? T
|
||||
if (casted != null) {
|
||||
trySend(BindServiceEvent.Connected(casted))
|
||||
trySend(Pair(casted, ""))
|
||||
} else {
|
||||
GlobalState.log("Binder is not of type ${T::class.java}")
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
trySend(Pair(null, "Binder is not of type ${T::class.java}"))
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
GlobalState.log("Failed to link to death: ${e.message}")
|
||||
binder.unlinkToDeath(deathRecipient, 0)
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
trySend(Pair(null, "Failed to link to death: ${e.message}"))
|
||||
}
|
||||
} else {
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
trySend(Pair(null, "Binder empty"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
GlobalState.log("Service disconnected")
|
||||
currentBinder?.unlinkToDeath(deathRecipient, 0)
|
||||
currentBinder = null
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
trySend(Pair(null, "Service disconnected"))
|
||||
}
|
||||
}
|
||||
|
||||
if (!bindService(intent, connection, flags)) {
|
||||
GlobalState.log("Failed to bind service")
|
||||
trySend(BindServiceEvent.Disconnected)
|
||||
close()
|
||||
return@callbackFlow
|
||||
val success = withContext(Dispatchers.Main) {
|
||||
bindService(intent, connection, flags)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
throw IllegalStateException("bindService() failed, will retry")
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
currentBinder?.unlinkToDeath(deathRecipient, 0)
|
||||
unbindService(connection)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
unbindService(connection)
|
||||
trySend(Pair(null, ""))
|
||||
}
|
||||
}
|
||||
}.retryWhen { cause, attempt ->
|
||||
if (attempt < maxRetries && cause is Exception) {
|
||||
delay(retryDelayMillis)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +243,7 @@ fun <T : List<ByteArray>> T.formatString(charset: Charset = Charsets.UTF_8): Str
|
||||
val totalSize = this.sumOf { it.size }
|
||||
val combined = ByteArray(totalSize)
|
||||
var offset = 0
|
||||
this.forEach { byteArray ->
|
||||
forEach { byteArray ->
|
||||
byteArray.copyInto(combined, offset)
|
||||
offset += byteArray.size
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.follow.clash.common
|
||||
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -12,16 +15,16 @@ object GlobalState : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
val packageName: String
|
||||
get() = _application.packageName
|
||||
get() = application.packageName
|
||||
|
||||
val RECEIVE_BROADCASTS_PERMISSIONS: String
|
||||
get() = "${packageName}.permission.RECEIVE_BROADCASTS"
|
||||
|
||||
|
||||
private lateinit var _application: Application
|
||||
private var _application: Application? = null
|
||||
|
||||
val application: Application
|
||||
get() = _application
|
||||
get() = _application!!
|
||||
|
||||
|
||||
fun log(text: String) {
|
||||
@@ -31,4 +34,14 @@ object GlobalState : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
fun init(application: Application) {
|
||||
_application = application
|
||||
}
|
||||
|
||||
fun setCrashlytics(enable: Boolean) {
|
||||
_application?.let {
|
||||
FirebaseApp.initializeApp(it)
|
||||
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = enable
|
||||
if (enable) {
|
||||
log("init crashlytics ${it.processName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,73 +6,70 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class ServiceDelegate<T>(
|
||||
private val intent: Intent,
|
||||
private val onServiceDisconnected: (() -> Unit)? = null,
|
||||
private val onServiceCrash: (() -> Unit)? = null,
|
||||
private val onServiceDisconnected: ((String) -> Unit)? = null,
|
||||
private val interfaceCreator: (IBinder) -> T,
|
||||
) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
|
||||
private val _service = MutableStateFlow<T?>(null)
|
||||
private val _bindingState = AtomicBoolean(false)
|
||||
|
||||
val service: StateFlow<T?> = _service
|
||||
private var _serviceState = MutableStateFlow<Pair<T?, String>?>(null)
|
||||
|
||||
private var bindJob: Job? = null
|
||||
private fun handleBindEvent(event: BindServiceEvent<IBinder>) {
|
||||
when (event) {
|
||||
is BindServiceEvent.Connected -> {
|
||||
_service.value = event.binder.let(interfaceCreator)
|
||||
}
|
||||
val serviceState: StateFlow<Pair<T?, String>?> = _serviceState
|
||||
private var job: Job? = null
|
||||
|
||||
is BindServiceEvent.Disconnected -> {
|
||||
_service.value = null
|
||||
onServiceDisconnected?.invoke()
|
||||
}
|
||||
|
||||
is BindServiceEvent.Crashed -> {
|
||||
_service.value = null
|
||||
onServiceCrash?.invoke()
|
||||
}
|
||||
private fun handleBind(data: Pair<IBinder?, String>) {
|
||||
data.first?.let {
|
||||
_serviceState.value = Pair(interfaceCreator(it), data.second)
|
||||
} ?: run {
|
||||
_serviceState.value = Pair(null, data.second)
|
||||
unbind()
|
||||
onServiceDisconnected?.invoke(data.second)
|
||||
_bindingState.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
unbind()
|
||||
bindJob = launch {
|
||||
GlobalState.application.bindServiceFlow<IBinder>(intent).collect { it ->
|
||||
handleBindEvent(it)
|
||||
if (_bindingState.compareAndSet(false, true)) {
|
||||
job?.cancel()
|
||||
job = null
|
||||
_serviceState.value = null
|
||||
job = launch {
|
||||
runCatching {
|
||||
GlobalState.application.bindServiceFlow<IBinder>(intent)
|
||||
.collect { handleBind(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <R> useService(
|
||||
retries: Int = 10,
|
||||
delayMillis: Long = 200,
|
||||
crossinline block: (T) -> R
|
||||
timeoutMillis: Long = 5000, crossinline block: suspend (T) -> R
|
||||
): Result<R> {
|
||||
return runCatching {
|
||||
service.filterNotNull()
|
||||
.retryWhen { _, attempt ->
|
||||
(attempt < retries).also {
|
||||
if (it) delay(delayMillis)
|
||||
}
|
||||
}.first().let {
|
||||
withTimeout(timeoutMillis) {
|
||||
val state = serviceState.filterNotNull().first()
|
||||
state.first?.let {
|
||||
block(it)
|
||||
}
|
||||
} ?: throw Exception(state.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
_service.value = null
|
||||
bindJob?.cancel()
|
||||
bindJob = null
|
||||
if (_bindingState.compareAndSet(true, false)) {
|
||||
job?.cancel()
|
||||
job = null
|
||||
_serviceState.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,7 @@ JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
|
||||
jstring stack, jstring address, jstring dns) {
|
||||
const auto interface = new_global(cb);
|
||||
scoped_string stackChar = get_string(stack);
|
||||
scoped_string addressChar = get_string(address);
|
||||
scoped_string dnsChar = get_string(dns);
|
||||
startTUN(interface, fd, stackChar, addressChar, dnsChar);
|
||||
startTUN(interface, fd, get_string(stack), get_string(address), get_string(dns));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
@@ -32,39 +29,39 @@ Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
|
||||
scoped_string dnsChar = get_string(dns);
|
||||
updateDns(dnsChar);
|
||||
updateDns(get_string(dns));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_invokeAction(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
|
||||
const auto interface = new_global(cb);
|
||||
scoped_string dataChar = get_string(data);
|
||||
invokeAction(interface, dataChar);
|
||||
invokeAction(interface, get_string(data));
|
||||
}
|
||||
|
||||
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"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
scoped_string res = getTraffic(only_statistics_proxy);
|
||||
return new_string(res);
|
||||
return new_string(getTraffic(only_statistics_proxy));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
scoped_string res = getTotalTraffic(only_statistics_proxy);
|
||||
return new_string(res);
|
||||
return new_string(getTotalTraffic(only_statistics_proxy));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
@@ -84,6 +81,10 @@ static void release_jni_object_impl(void *obj) {
|
||||
del_global(static_cast<jobject>(obj));
|
||||
}
|
||||
|
||||
static void free_string_impl(char *str) {
|
||||
free(str);
|
||||
}
|
||||
|
||||
static void call_tun_interface_protect_impl(void *tun_interface, const int fd) {
|
||||
ATTACH_JNI();
|
||||
env->CallVoidMethod(static_cast<jobject>(tun_interface),
|
||||
@@ -98,14 +99,13 @@ call_tun_interface_resolve_process_impl(void *tun_interface, const int protocol,
|
||||
const int uid) {
|
||||
ATTACH_JNI();
|
||||
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||
static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_process,
|
||||
protocol,
|
||||
new_string(source),
|
||||
new_string(target),
|
||||
uid));
|
||||
scoped_string packageNameChar = get_string(packageName);
|
||||
return packageNameChar;
|
||||
static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_process,
|
||||
protocol,
|
||||
new_string(source),
|
||||
new_string(target),
|
||||
uid));
|
||||
return get_string(packageName);
|
||||
}
|
||||
|
||||
static void call_invoke_interface_result_impl(void *invoke_interface, const char *data) {
|
||||
@@ -140,6 +140,7 @@ JNI_OnLoad(JavaVM *vm, void *) {
|
||||
resolve_process_func = &call_tun_interface_resolve_process_impl;
|
||||
result_func = &call_invoke_interface_result_impl;
|
||||
release_object_func = &release_jni_object_impl;
|
||||
free_string_func = &free_string_impl;
|
||||
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
@@ -172,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()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[versions]
|
||||
#agp = "8.10.1"
|
||||
firebaseBom = "34.2.0"
|
||||
minSdk = "23"
|
||||
targetSdk = "36"
|
||||
compileSdk = "36"
|
||||
@@ -10,11 +11,18 @@ coreSplashscreen = "1.0.1"
|
||||
gson = "2.13.1"
|
||||
kotlin = "2.2.10"
|
||||
smaliDexlib2 = "3.0.9"
|
||||
firebaseCrashlyticsKtx = "20.0.1"
|
||||
firebaseCommonKtx = "22.0.0"
|
||||
|
||||
[libraries]
|
||||
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
|
||||
annotation-jvm = { module = "androidx.annotation:annotation-jvm", version.ref = "annotationJvm" }
|
||||
core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
|
||||
firebase-crashlytics-ndk = { module = "com.google.firebase:firebase-crashlytics-ndk" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }
|
||||
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }
|
||||
firebase-crashlytics-ktx = { group = "com.google.firebase", name = "firebase-crashlytics-ktx", version.ref = "firebaseCrashlyticsKtx" }
|
||||
firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx", version.ref = "firebaseCommonKtx" }
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name="com.follow.clash.service.VpnService"
|
||||
android:name=".VpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:process=":background">
|
||||
android:process=":remote">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
@@ -19,18 +19,31 @@
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.follow.clash.service.CommonService"
|
||||
android:name=".CommonService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:process=":background">
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:process=":remote">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.follow.clash.service.RemoteService"
|
||||
android:name=".RemoteService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":background" />
|
||||
android:process=":remote" />
|
||||
|
||||
<provider
|
||||
android:name=".FilesProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:process=":remote">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -2,5 +2,5 @@
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface ICallbackInterface {
|
||||
void onResult(in byte[] result, boolean isSuccess);
|
||||
oneway void onResult(in byte[] data,in boolean isSuccess);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// IEventInterface.aidl
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface IEventInterface {
|
||||
oneway void onEvent(in String id, in byte[] data,in boolean isSuccess);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// IMessageInterface.aidl
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface IMessageInterface {
|
||||
void onResult(String result);
|
||||
}
|
||||
@@ -2,14 +2,17 @@
|
||||
package com.follow.clash.service;
|
||||
|
||||
import com.follow.clash.service.ICallbackInterface;
|
||||
import com.follow.clash.service.IMessageInterface;
|
||||
import com.follow.clash.service.IEventInterface;
|
||||
import com.follow.clash.service.IResultInterface;
|
||||
import com.follow.clash.service.models.VpnOptions;
|
||||
import com.follow.clash.service.models.NotificationParams;
|
||||
|
||||
interface IRemoteInterface {
|
||||
void invokeAction(in String data, in ICallbackInterface callback);
|
||||
void updateNotificationParams(in NotificationParams params);
|
||||
void startService(in VpnOptions options,in boolean inApp);
|
||||
void stopService();
|
||||
void setMessageCallback(in IMessageInterface messageCallback);
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// IResultInterface.aidl
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface IResultInterface {
|
||||
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,35 +1,33 @@
|
||||
package com.follow.clash
|
||||
package com.follow.clash.service
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.DocumentsProvider
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
class FilesProvider : DocumentsProvider() {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ROOT_ID = "0"
|
||||
|
||||
private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_FLAGS,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
)
|
||||
private val DEFAULT_ROOT_COLUMNS = arrayOf(
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID
|
||||
DocumentsContract.Root.COLUMN_ROOT_ID,
|
||||
DocumentsContract.Root.COLUMN_FLAGS,
|
||||
DocumentsContract.Root.COLUMN_ICON,
|
||||
DocumentsContract.Root.COLUMN_TITLE,
|
||||
DocumentsContract.Root.COLUMN_SUMMARY,
|
||||
DocumentsContract.Root.COLUMN_DOCUMENT_ID
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,12 +38,12 @@ class FilesProvider : DocumentsProvider() {
|
||||
override fun queryRoots(projection: Array<String>?): Cursor {
|
||||
return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply {
|
||||
newRow().apply {
|
||||
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY)
|
||||
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
add(Root.COLUMN_TITLE, "FlClash")
|
||||
add(Root.COLUMN_SUMMARY, "Data")
|
||||
add(Root.COLUMN_DOCUMENT_ID, "/")
|
||||
add(DocumentsContract.Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_LOCAL_ONLY)
|
||||
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_service)
|
||||
add(DocumentsContract.Root.COLUMN_TITLE, "FlClash")
|
||||
add(DocumentsContract.Root.COLUMN_SUMMARY, "Data")
|
||||
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,20 +85,20 @@ class FilesProvider : DocumentsProvider() {
|
||||
|
||||
private fun includeFile(result: MatrixCursor, file: File) {
|
||||
result.newRow().apply {
|
||||
add(Document.COLUMN_DOCUMENT_ID, file.absolutePath)
|
||||
add(Document.COLUMN_DISPLAY_NAME, file.name)
|
||||
add(Document.COLUMN_SIZE, file.length())
|
||||
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, file.absolutePath)
|
||||
add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.name)
|
||||
add(DocumentsContract.Document.COLUMN_SIZE, file.length())
|
||||
add(
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE
|
||||
DocumentsContract.Document.COLUMN_FLAGS,
|
||||
DocumentsContract.Document.FLAG_SUPPORTS_WRITE or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
|
||||
)
|
||||
add(Document.COLUMN_MIME_TYPE, getDocumentType(file))
|
||||
add(DocumentsContract.Document.COLUMN_MIME_TYPE, getDocumentType(file))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDocumentType(file: File): String {
|
||||
return if (file.isDirectory) {
|
||||
Document.MIME_TYPE_DIR
|
||||
DocumentsContract.Document.MIME_TYPE_DIR
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -3,62 +3,84 @@ package com.follow.clash.service
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.follow.clash.common.GlobalState
|
||||
import com.follow.clash.common.ServiceDelegate
|
||||
import com.follow.clash.common.chunkedForAidl
|
||||
import com.follow.clash.common.intent
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.service.State.delegate
|
||||
import com.follow.clash.service.State.intent
|
||||
import com.follow.clash.service.State.runLock
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.UUID
|
||||
|
||||
class RemoteService : Service(),
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
private var delegate: ServiceDelegate<IBaseService>? = null
|
||||
private var intent: Intent? = null
|
||||
|
||||
private fun handleStopService() {
|
||||
private fun handleStopService(result: IResultInterface) {
|
||||
launch {
|
||||
delegate?.useService { service ->
|
||||
service.stop()
|
||||
delegate?.unbind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartService() {
|
||||
launch {
|
||||
val nextIntent = when (State.options?.enable == true) {
|
||||
true -> VpnService::class.intent
|
||||
false -> CommonService::class.intent
|
||||
}
|
||||
if (intent != nextIntent) {
|
||||
delegate?.unbind()
|
||||
delegate = ServiceDelegate(nextIntent) { binder ->
|
||||
when (binder) {
|
||||
is VpnService.LocalBinder -> binder.getService()
|
||||
is CommonService.LocalBinder -> binder.getService()
|
||||
else -> throw IllegalArgumentException("Invalid binder type")
|
||||
}
|
||||
runLock.withLock {
|
||||
delegate?.useService { service ->
|
||||
service.stop()
|
||||
delegate?.unbind()
|
||||
}
|
||||
intent = nextIntent
|
||||
delegate?.bind()
|
||||
}
|
||||
delegate?.useService { service ->
|
||||
service.start()
|
||||
State.runTime = 0
|
||||
result.onResult(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val binder: IRemoteInterface.Stub = object : IRemoteInterface.Stub() {
|
||||
private fun handleServiceDisconnected(message: String) {
|
||||
GlobalState.log("Background service disconnected: $message")
|
||||
intent = null
|
||||
delegate = null
|
||||
}
|
||||
|
||||
private fun handleStartService(runTime: Long, result: IResultInterface) {
|
||||
launch {
|
||||
runLock.withLock {
|
||||
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")
|
||||
}
|
||||
}
|
||||
intent = nextIntent
|
||||
delegate?.bind()
|
||||
}
|
||||
delegate?.useService { service ->
|
||||
service.start()
|
||||
}
|
||||
State.runTime = when (runTime != 0L) {
|
||||
true -> runTime
|
||||
false -> System.currentTimeMillis()
|
||||
}
|
||||
result.onResult(State.runTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = object : IRemoteInterface.Stub() {
|
||||
override fun invokeAction(data: String, callback: ICallbackInterface) {
|
||||
Core.invokeAction(data) {
|
||||
val chunks = it?.chunkedForAidl() ?: listOf()
|
||||
val totalSize = chunks.size
|
||||
chunks.forEachIndexed { index, chunk ->
|
||||
callback.onResult(chunk, totalSize - 1 == index)
|
||||
runCatching {
|
||||
val chunks = it?.chunkedForAidl() ?: listOf()
|
||||
val totalSize = chunks.size
|
||||
chunks.forEachIndexed { index, chunk ->
|
||||
callback.onResult(chunk, totalSize - 1 == index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,27 +90,51 @@ class RemoteService : Service(),
|
||||
}
|
||||
|
||||
override fun startService(
|
||||
options: VpnOptions, inApp: Boolean
|
||||
options: VpnOptions,
|
||||
runtime: Long,
|
||||
result: IResultInterface,
|
||||
) {
|
||||
State.options = options
|
||||
State.inApp = inApp
|
||||
handleStartService()
|
||||
handleStartService(runtime, result)
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
handleStopService()
|
||||
override fun stopService(result: IResultInterface) {
|
||||
handleStopService(result)
|
||||
}
|
||||
|
||||
override fun setMessageCallback(messageCallback: IMessageInterface) {
|
||||
setMessageCallback(messageCallback::onResult)
|
||||
}
|
||||
}
|
||||
override fun setEventListener(eventListener: IEventInterface?) {
|
||||
GlobalState.log("RemoveEventListener ${eventListener == null}")
|
||||
when (eventListener != null) {
|
||||
true -> Core.callSetEventListener {
|
||||
runCatching {
|
||||
val id = UUID.randomUUID().toString()
|
||||
val chunks = it?.chunkedForAidl() ?: listOf()
|
||||
val totalSize = chunks.size
|
||||
chunks.forEachIndexed { index, chunk ->
|
||||
eventListener.onEvent(id, chunk, totalSize - 1 == index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMessageCallback(cb: (result: String?) -> Unit) {
|
||||
Core.setMessageCallback(cb)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -13,7 +14,12 @@ val Traffic.speedText: String
|
||||
get() = "${up.formatBytes}/s↑ ${down.formatBytes}/s↓"
|
||||
|
||||
fun Core.getSpeedTrafficText(onlyStatisticsProxy: Boolean): String {
|
||||
val res = getTraffic(onlyStatisticsProxy)
|
||||
val traffic = Gson().fromJson(res, Traffic::class.java)
|
||||
return traffic.speedText
|
||||
try {
|
||||
val res = getTraffic(onlyStatisticsProxy)
|
||||
val traffic = Gson().fromJson(res, Traffic::class.java)
|
||||
return traffic.speedText
|
||||
} catch (e: Exception) {
|
||||
GlobalState.log(e.message + "")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class NetworkObserveModule(private val service: Service) : Module() {
|
||||
return
|
||||
}
|
||||
preDnsList = dnsList
|
||||
Core.updateDNS(dnsList.joinToString { "," })
|
||||
Core.updateDNS(dnsList.toSet().joinToString(","))
|
||||
}
|
||||
|
||||
fun setUnderlyingNetworks(network: Network) {
|
||||
|
||||
@@ -47,9 +47,6 @@ class NotificationModule(private val service: Service) : Module() {
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun onInstall() {
|
||||
State.notificationParamsFlow.value?.let {
|
||||
update(it.extended)
|
||||
}
|
||||
scope.launch {
|
||||
val screenFlow = service.receiveBroadcastFlow {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
@@ -69,6 +66,12 @@ class NotificationModule(private val service: Service) : Module() {
|
||||
.collect { (params, _) ->
|
||||
update(params!!)
|
||||
}
|
||||
|
||||
State.notificationParamsFlow.value?.let {
|
||||
update(it.extended)
|
||||
} ?: run {
|
||||
update(NotificationParams().extended)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +93,13 @@ class NotificationModule(private val service: Service) : Module() {
|
||||
setSmallIcon(R.drawable.ic)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(intent.toPendingIntent)
|
||||
setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setShowWhen(true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
}
|
||||
@@ -105,7 +109,6 @@ class NotificationModule(private val service: Service) : Module() {
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(params.title)
|
||||
setContentText(params.contentText)
|
||||
setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
clearActions()
|
||||
addAction(
|
||||
0, params.stopText, QuickAction.STOP.quickIntent.toPendingIntent
|
||||
|
||||
17
android/service/src/main/res/drawable/ic_service.xml
Normal file
17
android/service/src/main/res/drawable/ic_service.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="240dp"
|
||||
android:height="240dp"
|
||||
android:viewportWidth="240"
|
||||
android:viewportHeight="240"
|
||||
tools:ignore="VectorRaster">
|
||||
<path
|
||||
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
|
||||
android:fillColor="#6666FB"/>
|
||||
<path
|
||||
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
|
||||
android:fillColor="#336AB6"/>
|
||||
<path
|
||||
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
|
||||
android:fillColor="#5CA8E9"/>
|
||||
</vector>
|
||||
@@ -18,8 +18,10 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.12.1" apply false
|
||||
id("com.android.application") version "8.12.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.10" apply false
|
||||
id("com.google.gms.google-services") version ("4.3.15") apply false
|
||||
id("com.google.firebase.crashlytics") version ("2.8.1") apply false
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
"autoSetSystemDns": "Auto set system DNS",
|
||||
"details": "{label} details",
|
||||
"creationTime": "Creation time",
|
||||
"progress": "Progress",
|
||||
"process": "Process",
|
||||
"host": "Host",
|
||||
"destination": "Destination",
|
||||
"destinationGeoIP": "Destination GeoIP",
|
||||
@@ -428,5 +428,9 @@
|
||||
"restartCoreTip": "Are you sure you want to restart the core?",
|
||||
"forceRestartCoreTip": "Are you sure you want to force restart the core?",
|
||||
"dnsHijacking": "DNS hijacking",
|
||||
"coreStatus": "Core status"
|
||||
"coreStatus": "Core status",
|
||||
"dataCollectionTip": "Data Collection Notice",
|
||||
"dataCollectionContent": "This app uses Firebase Crashlytics to collect crash information to improve app stability.\nThe collected data includes device information and crash details, but does not contain personal sensitive data.\nYou can disable this feature in settings.",
|
||||
"crashlytics": "Crash Analysis",
|
||||
"crashlyticsTip": "When enabled, automatically uploads crash logs without sensitive information when the app crashes"
|
||||
}
|
||||
@@ -410,7 +410,7 @@
|
||||
"autoSetSystemDns": "オートセットシステムDNS",
|
||||
"details": "{label}詳細",
|
||||
"creationTime": "作成時間",
|
||||
"progress": "進捗",
|
||||
"process": "プロセス",
|
||||
"host": "ホスト",
|
||||
"destination": "宛先",
|
||||
"destinationGeoIP": "宛先地理情報",
|
||||
@@ -429,5 +429,9 @@
|
||||
"restartCoreTip": "コアを再起動してもよろしいですか?",
|
||||
"forceRestartCoreTip": "コアを強制再起動してもよろしいですか?",
|
||||
"dnsHijacking": "DNSハイジャッキング",
|
||||
"coreStatus": "コアステータス"
|
||||
"coreStatus": "コアステータス",
|
||||
"dataCollectionTip": "データ収集説明",
|
||||
"dataCollectionContent": "本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。",
|
||||
"crashlytics": "クラッシュ分析",
|
||||
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします"
|
||||
}
|
||||
@@ -410,7 +410,7 @@
|
||||
"autoSetSystemDns": "Автоматическая настройка системного DNS",
|
||||
"details": "Детали {}",
|
||||
"creationTime": "Время создания",
|
||||
"progress": "Прогресс",
|
||||
"process": "процесс",
|
||||
"host": "Хост",
|
||||
"destination": "Назначение",
|
||||
"destinationGeoIP": "Геолокация назначения",
|
||||
@@ -429,5 +429,9 @@
|
||||
"restartCoreTip": "Вы уверены, что хотите перезапустить ядро?",
|
||||
"forceRestartCoreTip": "Вы уверены, что хотите принудительно перезапустить ядро?",
|
||||
"dnsHijacking": "DNS-перехват",
|
||||
"coreStatus": "Основной статус"
|
||||
"coreStatus": "Основной статус",
|
||||
"dataCollectionTip": "Уведомление о сборе данных",
|
||||
"dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.",
|
||||
"crashlytics": "Анализ сбоев",
|
||||
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя"
|
||||
}
|
||||
@@ -410,7 +410,7 @@
|
||||
"autoSetSystemDns": "自动设置系统DNS",
|
||||
"details": "{label}详情",
|
||||
"creationTime": "创建时间",
|
||||
"progress": "进度",
|
||||
"process": "进程",
|
||||
"host": "主机",
|
||||
"destination": "目标地址",
|
||||
"destinationGeoIP": "目标地理定位",
|
||||
@@ -429,5 +429,9 @@
|
||||
"restartCoreTip": "您确定要重启核心吗?",
|
||||
"forceRestartCoreTip": "您确定要强制重启核心吗?",
|
||||
"dnsHijacking": "DNS劫持",
|
||||
"coreStatus": "核心状态"
|
||||
"coreStatus": "核心状态",
|
||||
"dataCollectionTip": "数据收集说明",
|
||||
"dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情,不包含个人敏感数据。\n您可以在设置中关闭此功能。",
|
||||
"crashlytics": "崩溃分析",
|
||||
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
assets/data/GEOIP.metadb
Normal file
BIN
assets/data/GEOIP.metadb
Normal file
Binary file not shown.
45523
assets/data/GEOSITE.dat
Normal file
45523
assets/data/GEOSITE.dat
Normal file
File diff suppressed because one or more lines are too long
35960
assets/data/GeoSite.dat
35960
assets/data/GeoSite.dat
File diff suppressed because one or more lines are too long
Binary file not shown.
Submodule core/Clash.Meta updated: 52dfcca013...573489787b
@@ -2,6 +2,8 @@
|
||||
|
||||
void (*release_object_func)(void *obj);
|
||||
|
||||
void (*free_string_func)(char *data);
|
||||
|
||||
void (*protect_func)(void *tun_interface, int fd);
|
||||
|
||||
char* (*resolve_process_func)(void *tun_interface,int protocol, const char *source, const char *target, int uid);
|
||||
@@ -20,6 +22,10 @@ void release_object(void *obj) {
|
||||
release_object_func(obj);
|
||||
}
|
||||
|
||||
void free_string(char *data) {
|
||||
free_string_func(data);
|
||||
}
|
||||
|
||||
void result(void *invoke_Interface, const char *data) {
|
||||
return result_func(invoke_Interface, data);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func resolveProcess(callback unsafe.Pointer, protocol int, source, target string
|
||||
t := C.CString(target)
|
||||
defer C.free(unsafe.Pointer(t))
|
||||
res := C.resolve_process(callback, C.int(protocol), s, t, C.int(uid))
|
||||
return parseCString(res)
|
||||
return takeCString(res)
|
||||
}
|
||||
|
||||
func invokeResult(callback unsafe.Pointer, data string) {
|
||||
@@ -29,7 +29,7 @@ func releaseObject(callback unsafe.Pointer) {
|
||||
C.release_object(callback)
|
||||
}
|
||||
|
||||
func parseCString(s *C.char) string {
|
||||
//defer C.free(unsafe.Pointer(s))
|
||||
func takeCString(s *C.char) string {
|
||||
defer C.free_string(s)
|
||||
return C.GoString(s)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
extern void (*release_object_func)(void *obj);
|
||||
|
||||
extern void (*free_string_func)(char *data);
|
||||
|
||||
extern void (*protect_func)(void *tun_interface, int fd);
|
||||
|
||||
extern char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
|
||||
@@ -16,4 +18,6 @@ extern char* resolve_process(void *tun_interface, int protocol, const char *sour
|
||||
|
||||
extern void release_object(void *obj);
|
||||
|
||||
extern void free_string(char *data);
|
||||
|
||||
extern void result(void *invoke_Interface, const char *data);
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/metacubex/mihomo/constant/features"
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/hub/route"
|
||||
"github.com/metacubex/mihomo/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
@@ -116,11 +115,15 @@ func updateListeners() {
|
||||
listeners := currentConfig.Listeners
|
||||
general := currentConfig.General
|
||||
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
||||
listener.SetAllowLan(general.AllowLan)
|
||||
|
||||
allowLan := general.AllowLan
|
||||
listener.SetAllowLan(allowLan)
|
||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||
inbound.SetAllowedIPs(general.LanAllowedIPs)
|
||||
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
|
||||
listener.SetBindAddress(general.BindAddress)
|
||||
|
||||
bindAddress := general.BindAddress
|
||||
listener.SetBindAddress(bindAddress)
|
||||
listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
|
||||
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
|
||||
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
|
||||
@@ -236,12 +239,30 @@ func updateConfig(params *UpdateParams) {
|
||||
updateListeners()
|
||||
}
|
||||
|
||||
func parseWithPath(path string) (*config.Config, error) {
|
||||
buf, err := readFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawConfig := config.DefaultRawConfig()
|
||||
err = UnmarshalJson(buf, rawConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parseRawConfig, err := config.ParseRawConfig(rawConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseRawConfig, nil
|
||||
}
|
||||
|
||||
func setupConfig(params *SetupParams) error {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
var err error
|
||||
constant.DefaultTestURL = params.TestURL
|
||||
currentConfig, err = executor.ParseWithPath(filepath.Join(constant.Path.HomeDir(), "config.yaml"))
|
||||
currentConfig, err = parseWithPath(filepath.Join(constant.Path.HomeDir(), "config.json"))
|
||||
if err != nil {
|
||||
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module core
|
||||
|
||||
go 1.20
|
||||
go 1.25
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
|
||||
12
core/hub.go
12
core/hub.go
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/common/observable"
|
||||
@@ -61,6 +60,7 @@ func handleStopListener() bool {
|
||||
defer runLock.Unlock()
|
||||
isRunning = false
|
||||
listener.StopListener()
|
||||
resolver.ResetConnection()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func handleGetTraffic(onlyStatisticsProxy bool) string {
|
||||
}
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
log.Errorln("Error: %s", err)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
@@ -158,7 +158,7 @@ func handleGetTotalTraffic(onlyStatisticsProxy bool) string {
|
||||
}
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
log.Errorln("Error: %s", err)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
@@ -228,7 +228,7 @@ func handleGetConnections() string {
|
||||
snapshot := statistic.DefaultManager.Snapshot()
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
log.Errorln("Error: %s", err)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
@@ -323,13 +323,13 @@ func handleUpdateGeoData(geoType string, geoName string, fn func(value string))
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
case "GEOIP":
|
||||
err := updater.UpdateGeoIpWithPath(path)
|
||||
if err != nil {
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
case "GEOSITE":
|
||||
err := updater.UpdateGeoSiteWithPath(path)
|
||||
if err != nil {
|
||||
fn(err.Error())
|
||||
|
||||
30
core/lib.go
30
core/lib.go
@@ -27,7 +27,7 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var messageCallback unsafe.Pointer
|
||||
var eventListener unsafe.Pointer
|
||||
|
||||
type TunHandler struct {
|
||||
listener *sing_tun.Listener
|
||||
@@ -181,7 +181,7 @@ func nextHandle(action *Action, result ActionResult) bool {
|
||||
|
||||
//export invokeAction
|
||||
func invokeAction(callback unsafe.Pointer, paramsChar *C.char) {
|
||||
params := parseCString(paramsChar)
|
||||
params := takeCString(paramsChar)
|
||||
var action = &Action{}
|
||||
err := json.Unmarshal([]byte(params), action)
|
||||
if err != nil {
|
||||
@@ -198,35 +198,39 @@ func invokeAction(callback unsafe.Pointer, paramsChar *C.char) {
|
||||
|
||||
//export startTUN
|
||||
func startTUN(callback unsafe.Pointer, fd C.int, stackChar, addressChar, dnsChar *C.char) bool {
|
||||
handleStartTun(callback, int(fd), parseCString(stackChar), parseCString(addressChar), parseCString(dnsChar))
|
||||
handleStartTun(callback, int(fd), takeCString(stackChar), takeCString(addressChar), takeCString(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
|
||||
func getTotalTraffic(onlyStatisticsProxy bool) *C.char {
|
||||
return C.CString(handleGetTotalTraffic(onlyStatisticsProxy))
|
||||
data := C.CString(handleGetTotalTraffic(onlyStatisticsProxy))
|
||||
defer C.free(unsafe.Pointer(data))
|
||||
return data
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic(onlyStatisticsProxy bool) *C.char {
|
||||
return C.CString(handleGetTraffic(onlyStatisticsProxy))
|
||||
data := C.CString(handleGetTraffic(onlyStatisticsProxy))
|
||||
defer C.free(unsafe.Pointer(data))
|
||||
return data
|
||||
}
|
||||
|
||||
func sendMessage(message Message) {
|
||||
if messageCallback == nil {
|
||||
if eventListener == nil {
|
||||
return
|
||||
}
|
||||
result := ActionResult{
|
||||
Method: messageMethod,
|
||||
callback: messageCallback,
|
||||
callback: eventListener,
|
||||
Data: message,
|
||||
}
|
||||
result.send()
|
||||
@@ -249,5 +253,5 @@ func forceGC() {
|
||||
|
||||
//export updateDns
|
||||
func updateDns(s *C.char) {
|
||||
handleUpdateDns(parseCString(s))
|
||||
handleUpdateDns(takeCString(s))
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPlatformState(Widget child) {
|
||||
Widget _buildPlatformState({required Widget child}) {
|
||||
if (system.isDesktop) {
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
@@ -77,7 +77,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
return AndroidManager(child: TileManager(child: child));
|
||||
}
|
||||
|
||||
Widget _buildState(Widget child) {
|
||||
Widget _buildState({required Widget child}) {
|
||||
return AppStateManager(
|
||||
child: CoreManager(
|
||||
child: ConnectivityManager(
|
||||
@@ -94,70 +94,68 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlatformApp(Widget child) {
|
||||
Widget _buildPlatformApp({required Widget child}) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(child: child);
|
||||
}
|
||||
return VpnManager(child: child);
|
||||
}
|
||||
|
||||
Widget _buildApp(Widget child) {
|
||||
Widget _buildApp({required Widget child}) {
|
||||
return MessageManager(child: ThemeManager(child: child));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return _buildPlatformState(
|
||||
_buildState(
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale = ref.watch(
|
||||
appSettingProvider.select((state) => state.locale),
|
||||
);
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
builder: (_, child) {
|
||||
return AppEnvManager(
|
||||
child: _buildApp(
|
||||
AppSidebarContainer(child: _buildPlatformApp(child!)),
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: utils.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
return Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale = ref.watch(
|
||||
appSettingProvider.select((state) => state.locale),
|
||||
);
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
builder: (_, child) {
|
||||
return AppEnvManager(
|
||||
child: _buildApp(
|
||||
child: _buildPlatformState(
|
||||
child: _buildState(child: _buildPlatformApp(child: child!)),
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child!,
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
),
|
||||
),
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: utils.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child!,
|
||||
);
|
||||
},
|
||||
child: const HomePage(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'system.dart';
|
||||
|
||||
class Android {
|
||||
Future<void> init() async {
|
||||
await service?.init();
|
||||
await service?.syncAndroidState(globalState.getAndroidState());
|
||||
}
|
||||
}
|
||||
|
||||
final android = system.isAndroid ? Android() : null;
|
||||
185
lib/common/cache.dart
Normal file
185
lib/common/cache.dart
Normal file
@@ -0,0 +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 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,5 @@
|
||||
export 'android.dart';
|
||||
export 'app_localizations.dart';
|
||||
export 'cache.dart';
|
||||
export 'color.dart';
|
||||
export 'compute.dart';
|
||||
export 'constant.dart';
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
|
||||
import 'string.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
List<Group> computeSort({
|
||||
required List<Group> groups,
|
||||
@@ -107,8 +106,5 @@ List<Proxy> _sortOfDelay({
|
||||
}
|
||||
|
||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||
return List.of(proxies)..sort(
|
||||
(a, b) =>
|
||||
utils.sortByChar(utils.getPinyin(a.name), utils.getPinyin(b.name)),
|
||||
);
|
||||
return List.of(proxies)..sort((a, b) => a.name.compareTo(b.name));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
@@ -30,10 +32,10 @@ const animateDuration = Duration(milliseconds: 100);
|
||||
const midDuration = Duration(milliseconds: 200);
|
||||
const commonDuration = Duration(milliseconds: 300);
|
||||
const defaultUpdateDuration = Duration(days: 1);
|
||||
const mmdbFileName = 'geoip.metadb';
|
||||
const asnFileName = 'ASN.mmdb';
|
||||
const geoIpFileName = 'GeoIP.dat';
|
||||
const geoSiteFileName = 'GeoSite.dat';
|
||||
const MMDB = 'GEOIP.metadb';
|
||||
const ASN = 'ASN.mmdb';
|
||||
const GEOIP = 'GEOIP.dat';
|
||||
const GEOSITE = 'GEOSITE.dat';
|
||||
final double kHeaderHeight = system.isDesktop
|
||||
? !system.isMacOS
|
||||
? 40
|
||||
|
||||
@@ -7,28 +7,17 @@ extension BuildContextExtension on BuildContext {
|
||||
return findAncestorStateOfType<CommonScaffoldState>();
|
||||
}
|
||||
|
||||
Future<void>? showNotifier(String text) {
|
||||
void showNotifier(String text) {
|
||||
return findAncestorStateOfType<MessageManagerState>()?.message(text);
|
||||
}
|
||||
|
||||
void showSnackBar(
|
||||
String message, {
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||
final width = viewWidth;
|
||||
EdgeInsets margin;
|
||||
if (width < 600) {
|
||||
margin = const EdgeInsets.only(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
left: 16,
|
||||
);
|
||||
margin = const EdgeInsets.only(bottom: 16, right: 16, left: 16);
|
||||
} else {
|
||||
margin = EdgeInsets.only(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: width - 316,
|
||||
);
|
||||
margin = EdgeInsets.only(bottom: 16, left: 16, right: width - 316);
|
||||
}
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -76,8 +65,11 @@ extension BuildContextExtension on BuildContext {
|
||||
class BackHandleInherited extends InheritedWidget {
|
||||
final Function handleBack;
|
||||
|
||||
const BackHandleInherited(
|
||||
{super.key, required this.handleBack, required super.child});
|
||||
const BackHandleInherited({
|
||||
super.key,
|
||||
required this.handleBack,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static BackHandleInherited? of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<BackHandleInherited>();
|
||||
|
||||
@@ -36,16 +36,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ class Navigation {
|
||||
keep: false,
|
||||
icon: Icon(Icons.space_dashboard),
|
||||
label: PageLabel.dashboard,
|
||||
builder: (_) => const DashboardView(),
|
||||
builder: (_) =>
|
||||
const DashboardView(key: GlobalObjectKey(PageLabel.dashboard)),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.article),
|
||||
label: PageLabel.proxies,
|
||||
builder: (_) => const ProxiesView(),
|
||||
builder: (_) =>
|
||||
const ProxiesView(key: GlobalObjectKey(PageLabel.proxies)),
|
||||
modes: hasProxies
|
||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||
: [],
|
||||
@@ -28,19 +30,22 @@ class Navigation {
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.folder),
|
||||
label: PageLabel.profiles,
|
||||
builder: (_) => const ProfilesView(),
|
||||
builder: (_) =>
|
||||
const ProfilesView(key: GlobalObjectKey(PageLabel.profiles)),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: PageLabel.requests,
|
||||
builder: (_) => const RequestsView(),
|
||||
builder: (_) =>
|
||||
const RequestsView(key: GlobalObjectKey(PageLabel.requests)),
|
||||
description: 'requestsDesc',
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
label: PageLabel.connections,
|
||||
builder: (_) => const ConnectionsView(),
|
||||
builder: (_) =>
|
||||
const ConnectionsView(key: GlobalObjectKey(PageLabel.connections)),
|
||||
description: 'connectionsDesc',
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
@@ -48,13 +53,14 @@ class Navigation {
|
||||
icon: Icon(Icons.storage),
|
||||
label: PageLabel.resources,
|
||||
description: 'resourcesDesc',
|
||||
builder: (_) => const ResourcesView(),
|
||||
builder: (_) =>
|
||||
const ResourcesView(key: GlobalObjectKey(PageLabel.resources)),
|
||||
modes: [NavigationItemMode.more],
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.adb),
|
||||
label: PageLabel.logs,
|
||||
builder: (_) => const LogsView(),
|
||||
builder: (_) => const LogsView(key: GlobalObjectKey(PageLabel.logs)),
|
||||
description: 'logsDesc',
|
||||
modes: openLogs
|
||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||
@@ -63,7 +69,7 @@ class Navigation {
|
||||
NavigationItem(
|
||||
icon: Icon(Icons.construction),
|
||||
label: PageLabel.tools,
|
||||
builder: (_) => const ToolsView(),
|
||||
builder: (_) => const ToolsView(key: GlobalObjectKey(PageLabel.tools)),
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -68,7 +68,7 @@ class AppPath {
|
||||
|
||||
Future<String> get configFilePath async {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return join(homeDirPath, 'config.yaml');
|
||||
return join(homeDirPath, 'config.json');
|
||||
}
|
||||
|
||||
Future<String> get sharedPreferencesPath async {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class Request {
|
||||
late final Dio _dio;
|
||||
late final Dio dio;
|
||||
late final Dio _clashDio;
|
||||
String? userAgent;
|
||||
|
||||
Request() {
|
||||
_dio = Dio(BaseOptions(headers: {'User-Agent': browserUa}));
|
||||
dio = Dio(BaseOptions(headers: {'User-Agent': browserUa}));
|
||||
_clashDio = Dio();
|
||||
_clashDio.httpClientAdapter = IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
@@ -48,7 +48,7 @@ class Request {
|
||||
|
||||
Future<MemoryImage?> getImage(String url) async {
|
||||
if (url.isEmpty) return null;
|
||||
final response = await _dio.get<Uint8List>(
|
||||
final response = await dio.get<Uint8List>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
@@ -58,7 +58,7 @@ class Request {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> checkForUpdate() async {
|
||||
final response = await _dio.get(
|
||||
final response = await dio.get(
|
||||
'https://api.github.com/repos/$repository/releases/latest',
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
@@ -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) {
|
||||
@@ -107,7 +109,6 @@ class Request {
|
||||
handleFailRes();
|
||||
})
|
||||
.catchError((e) {
|
||||
print(e);
|
||||
failureCount++;
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
completer.complete(Result.error('cancelled'));
|
||||
@@ -123,7 +124,7 @@ class Request {
|
||||
|
||||
Future<bool> pingHelper() async {
|
||||
try {
|
||||
final response = await _dio
|
||||
final response = await dio
|
||||
.get(
|
||||
'http://$localhost:$helperPort/ping',
|
||||
options: Options(responseType: ResponseType.plain),
|
||||
@@ -140,7 +141,7 @@ class Request {
|
||||
|
||||
Future<bool> startCoreByHelper(String arg) async {
|
||||
try {
|
||||
final response = await _dio
|
||||
final response = await dio
|
||||
.post(
|
||||
'http://$localhost:$helperPort/start',
|
||||
data: json.encode({'path': appPath.corePath, 'arg': arg}),
|
||||
@@ -159,7 +160,7 @@ class Request {
|
||||
|
||||
Future<bool> stopCoreByHelper() async {
|
||||
try {
|
||||
final response = await _dio
|
||||
final response = await dio
|
||||
.post(
|
||||
'http://$localhost:$helperPort/stop',
|
||||
options: Options(responseType: ResponseType.plain),
|
||||
|
||||
@@ -8,13 +8,40 @@ import 'package:flutter/material.dart';
|
||||
class BaseScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.trackpad,
|
||||
if (system.isDesktop) PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.unknown,
|
||||
};
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.trackpad,
|
||||
if (system.isDesktop) PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.unknown,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget buildScrollbar(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
switch (axisDirectionToAxis(details.direction)) {
|
||||
case Axis.horizontal:
|
||||
return child;
|
||||
case Axis.vertical:
|
||||
switch (getPlatform(context)) {
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
assert(details.controller != null);
|
||||
return CommonScrollBar(
|
||||
controller: details.controller,
|
||||
child: child,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
||||
@@ -35,10 +62,7 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) {
|
||||
return CommonScrollBar(
|
||||
controller: details.controller,
|
||||
child: child,
|
||||
);
|
||||
return CommonScrollBar(controller: details.controller, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +76,9 @@ class NextClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
|
||||
@override
|
||||
Simulation? createBallisticSimulation(
|
||||
ScrollMetrics position, double velocity) {
|
||||
ScrollMetrics position,
|
||||
double velocity,
|
||||
) {
|
||||
final Tolerance tolerance = toleranceFor(position);
|
||||
if (position.outOfRange) {
|
||||
double? end;
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -37,7 +37,7 @@ class System {
|
||||
'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion,
|
||||
'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
|
||||
'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion,
|
||||
String() => 0
|
||||
String() => 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class System {
|
||||
);
|
||||
final arguments = [
|
||||
'-c',
|
||||
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
|
||||
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"',
|
||||
];
|
||||
final result = await Process.run(shell, arguments);
|
||||
if (result.exitCode != 0) {
|
||||
@@ -148,21 +148,25 @@ class Windows {
|
||||
final argumentsPtr = arguments.toNativeUtf16();
|
||||
final operationPtr = 'runas'.toNativeUtf16();
|
||||
|
||||
final shellExecute = _shell32.lookupFunction<
|
||||
Int32 Function(
|
||||
final shellExecute = _shell32
|
||||
.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
Int32 nShowCmd),
|
||||
int Function(
|
||||
Int32 nShowCmd,
|
||||
),
|
||||
int Function(
|
||||
Pointer<Utf16> hwnd,
|
||||
Pointer<Utf16> lpOperation,
|
||||
Pointer<Utf16> lpFile,
|
||||
Pointer<Utf16> lpParameters,
|
||||
Pointer<Utf16> lpDirectory,
|
||||
int nShowCmd)>('ShellExecuteW');
|
||||
int nShowCmd,
|
||||
)
|
||||
>('ShellExecuteW');
|
||||
|
||||
final result = shellExecute(
|
||||
nullptr,
|
||||
@@ -177,7 +181,10 @@ 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) {
|
||||
return false;
|
||||
@@ -248,15 +255,14 @@ class Windows {
|
||||
|
||||
final res = runas('cmd.exe', command);
|
||||
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: 300),
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool> registerTask(String appName) async {
|
||||
final taskXml = '''
|
||||
final taskXml =
|
||||
'''
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<Principals>
|
||||
@@ -295,8 +301,9 @@ class Windows {
|
||||
</Task>''';
|
||||
final taskPath = join(await appPath.tempPath, 'task.xml');
|
||||
await File(taskPath).create(recursive: true);
|
||||
await File(taskPath)
|
||||
.writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
|
||||
await File(
|
||||
taskPath,
|
||||
).writeAsBytes(taskXml.encodeUtf16LeWithBom, flush: true);
|
||||
final commandLine = [
|
||||
'/Create',
|
||||
'/TN',
|
||||
@@ -305,10 +312,7 @@ class Windows {
|
||||
'%s',
|
||||
'/F',
|
||||
].join(' ');
|
||||
return runas(
|
||||
'schtasks',
|
||||
commandLine.replaceFirst('%s', taskPath),
|
||||
);
|
||||
return runas('schtasks', commandLine.replaceFirst('%s', taskPath));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,23 +341,25 @@ class MacOS {
|
||||
return null;
|
||||
}
|
||||
final device = lineSplits[1];
|
||||
final serviceResult = await Process.run(
|
||||
'networksetup',
|
||||
['-listnetworkserviceorder'],
|
||||
);
|
||||
final serviceResult = await Process.run('networksetup', [
|
||||
'-listnetworkserviceorder',
|
||||
]);
|
||||
final serviceResultOutput = serviceResult.stdout.toString();
|
||||
final currentService = serviceResultOutput.split('\n\n').firstWhere(
|
||||
(s) => s.contains('Device: $device'),
|
||||
orElse: () => '',
|
||||
);
|
||||
final currentService = serviceResultOutput
|
||||
.split('\n\n')
|
||||
.firstWhere((s) => s.contains('Device: $device'), orElse: () => '');
|
||||
if (currentService.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final currentServiceNameLine = currentService.split('\n').firstWhere(
|
||||
(line) => RegExp(r'^\(\d+\).*').hasMatch(line),
|
||||
orElse: () => '');
|
||||
final currentServiceNameLineSplits =
|
||||
currentServiceNameLine.trim().split(' ');
|
||||
final currentServiceNameLine = currentService
|
||||
.split('\n')
|
||||
.firstWhere(
|
||||
(line) => RegExp(r'^\(\d+\).*').hasMatch(line),
|
||||
orElse: () => '',
|
||||
);
|
||||
final currentServiceNameLineSplits = currentServiceNameLine.trim().split(
|
||||
' ',
|
||||
);
|
||||
if (currentServiceNameLineSplits.length < 2) {
|
||||
return null;
|
||||
}
|
||||
@@ -365,10 +371,10 @@ class MacOS {
|
||||
if (deviceServiceName == null) {
|
||||
return null;
|
||||
}
|
||||
final result = await Process.run(
|
||||
'networksetup',
|
||||
['-getdnsservers', deviceServiceName],
|
||||
);
|
||||
final result = await Process.run('networksetup', [
|
||||
'-getdnsservers',
|
||||
deviceServiceName,
|
||||
]);
|
||||
final output = result.stdout.toString().trim();
|
||||
if (output.startsWith("There aren't any DNS Servers set on")) {
|
||||
originDns = [];
|
||||
@@ -400,15 +406,12 @@ class MacOS {
|
||||
if (nextDns == null) {
|
||||
return;
|
||||
}
|
||||
await Process.run(
|
||||
'networksetup',
|
||||
[
|
||||
'-setdnsservers',
|
||||
serviceName,
|
||||
if (nextDns.isNotEmpty) ...nextDns,
|
||||
if (nextDns.isEmpty) 'Empty',
|
||||
],
|
||||
);
|
||||
await Process.run('networksetup', [
|
||||
'-setdnsservers',
|
||||
serviceName,
|
||||
if (nextDns.isNotEmpty) ...nextDns,
|
||||
if (nextDns.isEmpty) 'Empty',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lpinyin/lpinyin.dart';
|
||||
|
||||
class Utils {
|
||||
Color? getDelayColor(int? delay) {
|
||||
@@ -178,11 +177,11 @@ class Utils {
|
||||
return build1.compareTo(build2);
|
||||
}
|
||||
|
||||
String getPinyin(String value) {
|
||||
return value.isNotEmpty
|
||||
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
|
||||
: '';
|
||||
}
|
||||
// String getPinyin(String value) {
|
||||
// return value.isNotEmpty
|
||||
// ? PinyinHelper.getFirstWordPinyin(value.substring(0, 1))
|
||||
// : '';
|
||||
// }
|
||||
|
||||
String? getFileNameForDisposition(String? disposition) {
|
||||
if (disposition == null) return null;
|
||||
@@ -228,7 +227,7 @@ class Utils {
|
||||
}
|
||||
|
||||
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
|
||||
final columns = max((viewWidth / 300).ceil(), 2);
|
||||
final columns = max((viewWidth / 250).ceil(), 2);
|
||||
return switch (proxiesLayout) {
|
||||
ProxiesLayout.tight => columns + 1,
|
||||
ProxiesLayout.standard => columns,
|
||||
@@ -237,7 +236,7 @@ class Utils {
|
||||
}
|
||||
|
||||
int getProfilesColumns(double viewWidth) {
|
||||
return max((viewWidth / 320).floor(), 1);
|
||||
return max((viewWidth / 280).floor(), 1);
|
||||
}
|
||||
|
||||
final _indexPrimary = [50, 100, 200, 300, 400, 500, 600, 700, 800, 850, 900];
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_acrylic/flutter_acrylic.dart' as acrylic;
|
||||
import 'package:screen_retriever/screen_retriever.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@@ -19,9 +18,6 @@ class Window {
|
||||
protocol.register('clashmeta');
|
||||
protocol.register('flclash');
|
||||
}
|
||||
if ((version > 10 && system.isMacOS)) {
|
||||
await acrylic.Window.initialize();
|
||||
}
|
||||
await windowManager.ensureInitialized();
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(props.width, props.height),
|
||||
@@ -39,25 +35,18 @@ class Window {
|
||||
await windowManager.setAlignment(Alignment.center);
|
||||
} else {
|
||||
final displays = await screenRetriever.getAllDisplays();
|
||||
final isPositionValid = displays.any(
|
||||
(display) {
|
||||
final displayBounds = Rect.fromLTWH(
|
||||
display.visiblePosition!.dx,
|
||||
display.visiblePosition!.dy,
|
||||
display.size.width,
|
||||
display.size.height,
|
||||
);
|
||||
return displayBounds.contains(Offset(left, top)) ||
|
||||
displayBounds.contains(Offset(right, bottom));
|
||||
},
|
||||
);
|
||||
if (isPositionValid) {
|
||||
await windowManager.setPosition(
|
||||
Offset(
|
||||
left,
|
||||
top,
|
||||
),
|
||||
final isPositionValid = displays.any((display) {
|
||||
final displayBounds = Rect.fromLTWH(
|
||||
display.visiblePosition!.dx,
|
||||
display.visiblePosition!.dy,
|
||||
display.size.width,
|
||||
display.size.height,
|
||||
);
|
||||
return displayBounds.contains(Offset(left, top)) ||
|
||||
displayBounds.contains(Offset(right, bottom));
|
||||
});
|
||||
if (isPositionValid) {
|
||||
await windowManager.setPosition(Offset(left, top));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,13 +55,6 @@ class Window {
|
||||
});
|
||||
}
|
||||
|
||||
void updateMacOSBrightness(Brightness brightness) {
|
||||
if (!system.isMacOS) {
|
||||
return;
|
||||
}
|
||||
acrylic.Window.overrideMacOSBrightness(dark: brightness == Brightness.dark);
|
||||
}
|
||||
|
||||
Future<void> show() async {
|
||||
render?.resume();
|
||||
await windowManager.show();
|
||||
|
||||
@@ -72,11 +72,11 @@ class AppController {
|
||||
}
|
||||
|
||||
Future<void> restartCore() async {
|
||||
globalState.isUserDisconnected = true;
|
||||
await coreController.shutdown();
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
|
||||
await coreController.preload();
|
||||
await _connectCore();
|
||||
await _initCore();
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connected;
|
||||
_ref.read(initProvider.notifier).value = true;
|
||||
if (_ref.read(isStartProvider)) {
|
||||
await globalState.handleStart();
|
||||
}
|
||||
@@ -353,7 +353,7 @@ class AppController {
|
||||
try {
|
||||
await updateProfile(profile);
|
||||
} catch (e) {
|
||||
commonPrint.log(e.toString());
|
||||
commonPrint.log(e.toString(), logLevel: LogLevel.warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,18 +527,15 @@ class AppController {
|
||||
|
||||
Future<void> init() async {
|
||||
FlutterError.onError = (details) {
|
||||
if (kDebugMode) {
|
||||
commonPrint.log(
|
||||
'exception: ${details.exception} stack: ${details.stack}',
|
||||
);
|
||||
}
|
||||
commonPrint.log(
|
||||
'exception: ${details.exception} stack: ${details.stack}',
|
||||
logLevel: LogLevel.warning,
|
||||
);
|
||||
};
|
||||
updateTray(true);
|
||||
await _initCore();
|
||||
await _initStatus();
|
||||
autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch);
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
autoLaunch?.updateStatus(_ref.read(appSettingProvider).autoLaunch);
|
||||
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
@@ -546,9 +543,31 @@ class AppController {
|
||||
}
|
||||
await _handlePreference();
|
||||
await _handlerDisclaimer();
|
||||
await _showCrashlyticsTip();
|
||||
await _connectCore();
|
||||
await _initCore();
|
||||
await _initStatus();
|
||||
_ref.read(initProvider.notifier).value = true;
|
||||
}
|
||||
|
||||
Future<void> _connectCore() async {
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
|
||||
final result = await Future.wait([
|
||||
coreController.preload(),
|
||||
if (!globalState.isService) Future.delayed(Duration(milliseconds: 300)),
|
||||
]);
|
||||
final String message = result[0];
|
||||
await Future.delayed(commonDuration);
|
||||
if (message.isNotEmpty) {
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
if (context.mounted) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connected;
|
||||
}
|
||||
|
||||
Future<void> _initStatus() async {
|
||||
if (system.isAndroid) {
|
||||
await globalState.updateStartTime();
|
||||
@@ -618,30 +637,47 @@ class AppController {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.updateState(
|
||||
(state) => state.copyWith(disclaimerAccepted: true),
|
||||
);
|
||||
Navigator.of(context).pop<bool>(true);
|
||||
},
|
||||
child: Text(appLocalizations.agree),
|
||||
),
|
||||
],
|
||||
child: SelectableText(appLocalizations.disclaimerDesc),
|
||||
child: Text(appLocalizations.disclaimerDesc),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<void> _showCrashlyticsTip() async {
|
||||
if (!system.isAndroid) {
|
||||
return;
|
||||
}
|
||||
if (_ref.read(appSettingProvider.select((state) => state.crashlyticsTip))) {
|
||||
return;
|
||||
}
|
||||
await globalState.showMessage(
|
||||
title: appLocalizations.dataCollectionTip,
|
||||
cancelable: false,
|
||||
message: TextSpan(text: appLocalizations.dataCollectionContent),
|
||||
);
|
||||
_ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.updateState((state) => state.copyWith(crashlyticsTip: true));
|
||||
}
|
||||
|
||||
Future<void> _handlerDisclaimer() async {
|
||||
if (_ref.read(appSettingProvider).disclaimerAccepted) {
|
||||
if (_ref.read(
|
||||
appSettingProvider.select((state) => state.disclaimerAccepted),
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
final isDisclaimerAccepted = await showDisclaimer();
|
||||
if (!isDisclaimerAccepted) {
|
||||
await handleExit();
|
||||
}
|
||||
_ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.updateState((state) => state.copyWith(disclaimerAccepted: true));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -926,7 +962,7 @@ class AppController {
|
||||
final res = await futureFunction();
|
||||
return res;
|
||||
} catch (e) {
|
||||
commonPrint.log('$futureFunction ===> $e');
|
||||
commonPrint.log('$futureFunction ===> $e', logLevel: LogLevel.warning);
|
||||
if (realSilence) {
|
||||
globalState.showNotifier(e.toString());
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ class CoreController {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<bool> preload() {
|
||||
Future<String> preload() {
|
||||
return _interface.preload();
|
||||
}
|
||||
|
||||
@@ -40,12 +40,7 @@ class CoreController {
|
||||
if (!isExists) {
|
||||
await homeDir.create(recursive: true);
|
||||
}
|
||||
const geoFileNameList = [
|
||||
mmdbFileName,
|
||||
geoIpFileName,
|
||||
geoSiteFileName,
|
||||
asnFileName,
|
||||
];
|
||||
const geoFileNameList = [MMDB, GEOIP, GEOSITE, ASN];
|
||||
try {
|
||||
for (final geoFileName in geoFileNameList) {
|
||||
final geoFile = File(join(homePath, geoFileName));
|
||||
|
||||
@@ -13,7 +13,7 @@ abstract mixin class CoreEventListener {
|
||||
|
||||
void onLoaded(String providerName) {}
|
||||
|
||||
void onCrash() {}
|
||||
void onCrash(String message) {}
|
||||
}
|
||||
|
||||
class CoreEventManager {
|
||||
@@ -36,7 +36,7 @@ class CoreEventManager {
|
||||
listener.onLoaded(event.data);
|
||||
break;
|
||||
case CoreEventType.crash:
|
||||
listener.onCrash();
|
||||
listener.onCrash(event.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:fl_clash/models/models.dart';
|
||||
mixin CoreInterface {
|
||||
Future<bool> init(InitParams params);
|
||||
|
||||
Future<bool> preload();
|
||||
Future<String> preload();
|
||||
|
||||
Future<bool> shutdown();
|
||||
|
||||
@@ -306,6 +306,7 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
return await _invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(seconds: 6),
|
||||
) ??
|
||||
json.encode(Delay(name: proxyName, value: -1, url: url));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'interface.dart';
|
||||
|
||||
@@ -15,10 +16,16 @@ class CoreLib extends CoreHandlerInterface {
|
||||
CoreLib._internal();
|
||||
|
||||
@override
|
||||
Future<bool> preload() async {
|
||||
await service?.init();
|
||||
Future<String> preload() async {
|
||||
final res = await service?.init();
|
||||
if (res?.isEmpty != true) {
|
||||
return res ?? '';
|
||||
}
|
||||
_connectedCompleter.complete(true);
|
||||
return true;
|
||||
final syncRes = await service?.syncAndroidState(
|
||||
globalState.getAndroidState(),
|
||||
);
|
||||
return syncRes ?? '';
|
||||
}
|
||||
|
||||
factory CoreLib() {
|
||||
@@ -33,6 +40,7 @@ class CoreLib extends CoreHandlerInterface {
|
||||
|
||||
@override
|
||||
Future<bool> shutdown() async {
|
||||
await service?.shutdown();
|
||||
_connectedCompleter = Completer();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
},
|
||||
(error, stack) async {
|
||||
commonPrint.log('Service error: $error');
|
||||
commonPrint.log('Service error: $error', logLevel: LogLevel.warning);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,9 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
void _handleInvokeCrashEvent() {
|
||||
coreEventManager.sendEvent(CoreEvent(type: CoreEventType.crash));
|
||||
coreEventManager.sendEvent(
|
||||
CoreEvent(type: CoreEventType.crash, data: 'socket done'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
@@ -95,9 +97,10 @@ 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;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -124,9 +127,9 @@ class CoreService extends CoreHandlerInterface {
|
||||
|
||||
Future<void> _destroySocket() async {
|
||||
if (_socketCompleter.isCompleted) {
|
||||
final lastSocket = await _socketCompleter.future;
|
||||
final socket = await _socketCompleter.future;
|
||||
_socketCompleter = Completer();
|
||||
lastSocket.close();
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +152,10 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> preload() async {
|
||||
Future<String> preload() async {
|
||||
await _serverCompleter.future;
|
||||
await start();
|
||||
return true;
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -177,7 +180,9 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future get connected => _socketCompleter.future;
|
||||
Future get connected {
|
||||
return _socketCompleter.future;
|
||||
}
|
||||
}
|
||||
|
||||
final coreService = system.isDesktop ? CoreService() : null;
|
||||
|
||||
@@ -199,11 +199,21 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"coreStatus": MessageLookupByLibrary.simpleMessage("Core status"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
||||
"crashTest": MessageLookupByLibrary.simpleMessage("Crash test"),
|
||||
"crashlytics": MessageLookupByLibrary.simpleMessage("Crash Analysis"),
|
||||
"crashlyticsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"When enabled, automatically uploads crash logs without sensitive information when the app crashes",
|
||||
),
|
||||
"create": MessageLookupByLibrary.simpleMessage("Create"),
|
||||
"creationTime": MessageLookupByLibrary.simpleMessage("Creation time"),
|
||||
"cut": MessageLookupByLibrary.simpleMessage("Cut"),
|
||||
"dark": MessageLookupByLibrary.simpleMessage("Dark"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"),
|
||||
"dataCollectionContent": MessageLookupByLibrary.simpleMessage(
|
||||
"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.",
|
||||
),
|
||||
"dataCollectionTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Data Collection Notice",
|
||||
),
|
||||
"days": MessageLookupByLibrary.simpleMessage("Days"),
|
||||
"defaultNameserver": MessageLookupByLibrary.simpleMessage(
|
||||
"Default nameserver",
|
||||
@@ -521,6 +531,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Please press the keyboard.",
|
||||
),
|
||||
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
|
||||
"process": MessageLookupByLibrary.simpleMessage("Process"),
|
||||
"profile": MessageLookupByLibrary.simpleMessage("Profile"),
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
@@ -547,7 +558,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
|
||||
"progress": MessageLookupByLibrary.simpleMessage("Progress"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("Project"),
|
||||
"providers": MessageLookupByLibrary.simpleMessage("Providers"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),
|
||||
|
||||
@@ -151,11 +151,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"coreStatus": MessageLookupByLibrary.simpleMessage("コアステータス"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("国"),
|
||||
"crashTest": MessageLookupByLibrary.simpleMessage("クラッシュテスト"),
|
||||
"crashlytics": MessageLookupByLibrary.simpleMessage("クラッシュ分析"),
|
||||
"crashlyticsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします",
|
||||
),
|
||||
"create": MessageLookupByLibrary.simpleMessage("作成"),
|
||||
"creationTime": MessageLookupByLibrary.simpleMessage("作成時間"),
|
||||
"cut": MessageLookupByLibrary.simpleMessage("切り取り"),
|
||||
"dark": MessageLookupByLibrary.simpleMessage("ダーク"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("ダッシュボード"),
|
||||
"dataCollectionContent": MessageLookupByLibrary.simpleMessage(
|
||||
"本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。",
|
||||
),
|
||||
"dataCollectionTip": MessageLookupByLibrary.simpleMessage("データ収集説明"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("日"),
|
||||
"defaultNameserver": MessageLookupByLibrary.simpleMessage("デフォルトネームサーバー"),
|
||||
"defaultNameserverDesc": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -395,6 +403,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage("DOHのHTTP/3を優先使用"),
|
||||
"pressKeyboard": MessageLookupByLibrary.simpleMessage("キーボードを押してください"),
|
||||
"preview": MessageLookupByLibrary.simpleMessage("プレビュー"),
|
||||
"process": MessageLookupByLibrary.simpleMessage("プロセス"),
|
||||
"profile": MessageLookupByLibrary.simpleMessage("プロファイル"),
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("有効な間隔形式を入力してください"),
|
||||
@@ -417,7 +426,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("プロファイル一覧"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("プロファイルの並び替え"),
|
||||
"progress": MessageLookupByLibrary.simpleMessage("進捗"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("プロジェクト"),
|
||||
"providers": MessageLookupByLibrary.simpleMessage("プロバイダー"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("プロキシ"),
|
||||
|
||||
@@ -204,11 +204,21 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"coreStatus": MessageLookupByLibrary.simpleMessage("Основной статус"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Страна"),
|
||||
"crashTest": MessageLookupByLibrary.simpleMessage("Тест на сбои"),
|
||||
"crashlytics": MessageLookupByLibrary.simpleMessage("Анализ сбоев"),
|
||||
"crashlyticsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя",
|
||||
),
|
||||
"create": MessageLookupByLibrary.simpleMessage("Создать"),
|
||||
"creationTime": MessageLookupByLibrary.simpleMessage("Время создания"),
|
||||
"cut": MessageLookupByLibrary.simpleMessage("Вырезать"),
|
||||
"dark": MessageLookupByLibrary.simpleMessage("Темный"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("Панель управления"),
|
||||
"dataCollectionContent": MessageLookupByLibrary.simpleMessage(
|
||||
"Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.",
|
||||
),
|
||||
"dataCollectionTip": MessageLookupByLibrary.simpleMessage(
|
||||
"Уведомление о сборе данных",
|
||||
),
|
||||
"days": MessageLookupByLibrary.simpleMessage("Дней"),
|
||||
"defaultNameserver": MessageLookupByLibrary.simpleMessage(
|
||||
"Сервер имен по умолчанию",
|
||||
@@ -548,6 +558,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Пожалуйста, нажмите клавишу.",
|
||||
),
|
||||
"preview": MessageLookupByLibrary.simpleMessage("Предпросмотр"),
|
||||
"process": MessageLookupByLibrary.simpleMessage("процесс"),
|
||||
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
@@ -574,7 +585,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("Профили"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("Сортировка профилей"),
|
||||
"progress": MessageLookupByLibrary.simpleMessage("Прогресс"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("Проект"),
|
||||
"providers": MessageLookupByLibrary.simpleMessage("Провайдеры"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("Прокси"),
|
||||
|
||||
@@ -141,11 +141,19 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"coreStatus": MessageLookupByLibrary.simpleMessage("核心状态"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("区域"),
|
||||
"crashTest": MessageLookupByLibrary.simpleMessage("崩溃测试"),
|
||||
"crashlytics": MessageLookupByLibrary.simpleMessage("崩溃分析"),
|
||||
"crashlyticsTip": MessageLookupByLibrary.simpleMessage(
|
||||
"开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志",
|
||||
),
|
||||
"create": MessageLookupByLibrary.simpleMessage("创建"),
|
||||
"creationTime": MessageLookupByLibrary.simpleMessage("创建时间"),
|
||||
"cut": MessageLookupByLibrary.simpleMessage("剪切"),
|
||||
"dark": MessageLookupByLibrary.simpleMessage("深色"),
|
||||
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
|
||||
"dataCollectionContent": MessageLookupByLibrary.simpleMessage(
|
||||
"本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情,不包含个人敏感数据。\n您可以在设置中关闭此功能。",
|
||||
),
|
||||
"dataCollectionTip": MessageLookupByLibrary.simpleMessage("数据收集说明"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("天"),
|
||||
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
|
||||
"defaultNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
|
||||
@@ -347,6 +355,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
|
||||
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
|
||||
"preview": MessageLookupByLibrary.simpleMessage("预览"),
|
||||
"process": MessageLookupByLibrary.simpleMessage("进程"),
|
||||
"profile": MessageLookupByLibrary.simpleMessage("配置"),
|
||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
|
||||
@@ -367,7 +376,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
),
|
||||
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
|
||||
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
|
||||
"progress": MessageLookupByLibrary.simpleMessage("进度"),
|
||||
"project": MessageLookupByLibrary.simpleMessage("项目"),
|
||||
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
|
||||
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
|
||||
|
||||
@@ -3159,9 +3159,9 @@ class AppLocalizations {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Progress`
|
||||
String get progress {
|
||||
return Intl.message('Progress', name: 'progress', desc: '', args: []);
|
||||
/// `Process`
|
||||
String get process {
|
||||
return Intl.message('Process', name: 'process', desc: '', args: []);
|
||||
}
|
||||
|
||||
/// `Host`
|
||||
@@ -3318,6 +3318,46 @@ class AppLocalizations {
|
||||
String get coreStatus {
|
||||
return Intl.message('Core status', name: 'coreStatus', desc: '', args: []);
|
||||
}
|
||||
|
||||
/// `Data Collection Notice`
|
||||
String get dataCollectionTip {
|
||||
return Intl.message(
|
||||
'Data Collection Notice',
|
||||
name: 'dataCollectionTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `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.`
|
||||
String get dataCollectionContent {
|
||||
return Intl.message(
|
||||
'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.',
|
||||
name: 'dataCollectionContent',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Crash Analysis`
|
||||
String get crashlytics {
|
||||
return Intl.message(
|
||||
'Crash Analysis',
|
||||
name: 'crashlytics',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `When enabled, automatically uploads crash logs without sensitive information when the app crashes`
|
||||
String get crashlyticsTip {
|
||||
return Intl.message(
|
||||
'When enabled, automatically uploads crash logs without sensitive information when the app crashes',
|
||||
name: 'crashlyticsTip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -23,8 +22,9 @@ Future<void> main() async {
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _service(List<String> flags) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isService = true;
|
||||
await globalState.init();
|
||||
await service?.init();
|
||||
await coreController.preload();
|
||||
tile?.addListener(
|
||||
_TileListenerWithService(
|
||||
onStop: () async {
|
||||
|
||||
@@ -48,9 +48,11 @@ class _AndroidContainerState extends ConsumerState<AndroidManager>
|
||||
}
|
||||
|
||||
@override
|
||||
void onServiceCrash() {
|
||||
coreEventManager.sendEvent(CoreEvent(type: CoreEventType.crash));
|
||||
super.onServiceCrash();
|
||||
void onServiceCrash(String message) {
|
||||
coreEventManager.sendEvent(
|
||||
CoreEvent(type: CoreEventType.crash, data: message),
|
||||
);
|
||||
super.onServiceCrash(message);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_acrylic/widgets/transparent_macos_sidebar.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -45,7 +44,7 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
|
||||
});
|
||||
ref.listenManual(needUpdateGroupsProvider, (prev, next) {
|
||||
if (prev != next) {
|
||||
globalState.appController.updateGroupsDebounce(commonDuration);
|
||||
globalState.appController.updateGroupsDebounce();
|
||||
}
|
||||
});
|
||||
if (window == null) {
|
||||
@@ -61,12 +60,6 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
|
||||
macOS?.updateDns(true);
|
||||
}
|
||||
});
|
||||
ref.listenManual(currentBrightnessProvider, (prev, next) {
|
||||
if (prev == next) {
|
||||
return;
|
||||
}
|
||||
window?.updateMacOSBrightness(next);
|
||||
}, fireImmediately: true);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -158,15 +151,25 @@ class AppSidebarContainer extends ConsumerWidget {
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
}) {
|
||||
if (!system.isMacOS) {
|
||||
return Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return TransparentMacOSSidebar(
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
);
|
||||
return Material(color: context.colorScheme.surfaceContainer, child: child);
|
||||
// if (!system.isMacOS) {
|
||||
// return Material(
|
||||
// color: context.colorScheme.surfaceContainer,
|
||||
// child: child,
|
||||
// );
|
||||
// }
|
||||
// return child;
|
||||
// return TransparentMacOSSidebar(
|
||||
// child: Material(color: Colors.transparent, child: child),
|
||||
// );
|
||||
}
|
||||
|
||||
void _updateSideBarWidth(WidgetRef ref, double contentWidth) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(sideWidthProvider.notifier).value =
|
||||
ref.read(viewSizeProvider.select((state) => state.width)) -
|
||||
contentWidth;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -181,76 +184,102 @@ class AppSidebarContainer extends ConsumerWidget {
|
||||
final showLabel = ref.watch(appSettingProvider).showLabel;
|
||||
return Row(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
_buildBackground(
|
||||
context: context,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 32),
|
||||
if (!system.isMacOS) ...[AppIcon(), SizedBox(height: 12)],
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: SingleChildScrollView(
|
||||
child: IntrinsicHeight(
|
||||
child: NavigationRail(
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: context.colorScheme.onSurface),
|
||||
unselectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(color: context.colorScheme.onSurface),
|
||||
destinations: navigationItems
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: e.icon,
|
||||
label: Text(Intl.message(e.label.name)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) {
|
||||
globalState.appController.toPage(
|
||||
navigationItems[index].label,
|
||||
);
|
||||
},
|
||||
extended: false,
|
||||
selectedIndex: currentIndex,
|
||||
labelType: showLabel
|
||||
? NavigationRailLabelType.all
|
||||
: NavigationRailLabelType.none,
|
||||
),
|
||||
_buildBackground(
|
||||
context: context,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (system.isMacOS) SizedBox(height: 22),
|
||||
SizedBox(height: 10),
|
||||
if (!system.isMacOS) ...[
|
||||
ClipRect(child: AppIcon()),
|
||||
SizedBox(height: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: HiddenBarScrollBehavior(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: NavigationRail(
|
||||
scrollable: true,
|
||||
minExtendedWidth: 200,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
unselectedLabelTextStyle: context
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
destinations: navigationItems
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: e.icon,
|
||||
label: Text(Intl.message(e.label.name)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) {
|
||||
globalState.appController.toPage(
|
||||
navigationItems[index].label,
|
||||
);
|
||||
},
|
||||
extended: false,
|
||||
selectedIndex: currentIndex,
|
||||
labelType: showLabel
|
||||
? NavigationRailLabelType.all
|
||||
: NavigationRailLabelType.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.updateState(
|
||||
(state) =>
|
||||
state.copyWith(showLabel: !state.showLabel),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.menu,
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
const SizedBox(height: 16),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.updateState(
|
||||
(state) =>
|
||||
state.copyWith(showLabel: !state.showLabel),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.menu,
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
_buildLoading(),
|
||||
],
|
||||
),
|
||||
_buildLoading(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ClipRect(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
_updateSideBarWidth(ref, constraints.maxWidth);
|
||||
return child;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(flex: 1, child: ClipRect(child: child)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,13 +92,16 @@ class _CoreContainerState extends ConsumerState<CoreManager>
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onCrash() async {
|
||||
Future<void> onCrash(String message) async {
|
||||
if (!globalState.isUserDisconnected) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
globalState.isUserDisconnected = false;
|
||||
if (ref.read(coreStatusProvider) != CoreStatus.connected) {
|
||||
return;
|
||||
}
|
||||
context.showNotifier('Core crash');
|
||||
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
await coreController.shutdown();
|
||||
super.onCrash();
|
||||
super.onCrash(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
@@ -17,8 +18,9 @@ class MessageManager extends StatefulWidget {
|
||||
|
||||
class MessageManagerState extends State<MessageManager> {
|
||||
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
|
||||
final List<CommonMessage> _bufferMessages = [];
|
||||
bool _pushing = false;
|
||||
final _bufferMessages = Queue<CommonMessage>();
|
||||
final _activeTimers = <String, Timer>{};
|
||||
bool _isDisplayingMessage = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -28,38 +30,48 @@ class MessageManagerState extends State<MessageManager> {
|
||||
@override
|
||||
void dispose() {
|
||||
_messagesNotifier.dispose();
|
||||
for (final timer in _activeTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_activeTimers.clear();
|
||||
_bufferMessages.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> message(String text) async {
|
||||
void message(String text) {
|
||||
final commonMessage = CommonMessage(id: utils.uuidV4, text: text);
|
||||
commonPrint.log(text);
|
||||
_bufferMessages.add(commonMessage);
|
||||
await _showMessage();
|
||||
commonPrint.log('message: $text');
|
||||
_processQueue();
|
||||
}
|
||||
|
||||
Future<void> _showMessage() async {
|
||||
if (_pushing == true) {
|
||||
void _cancelMessage(String id) {
|
||||
_bufferMessages.removeWhere((msg) => msg.id == id);
|
||||
if (_activeTimers.containsKey(id)) {
|
||||
_removeMessage(id);
|
||||
}
|
||||
}
|
||||
|
||||
void _processQueue() {
|
||||
if (_isDisplayingMessage || _bufferMessages.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_pushing = true;
|
||||
while (_bufferMessages.isNotEmpty) {
|
||||
final commonMessage = _bufferMessages.removeAt(0);
|
||||
_messagesNotifier.value = List.from(_messagesNotifier.value)
|
||||
..add(commonMessage);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
Future.delayed(commonMessage.duration, () {
|
||||
_handleRemove(commonMessage);
|
||||
});
|
||||
}
|
||||
_isDisplayingMessage = true;
|
||||
final message = _bufferMessages.removeFirst();
|
||||
_messagesNotifier.value = List.from(_messagesNotifier.value)..add(message);
|
||||
final timer = Timer(message.duration, () {
|
||||
_removeMessage(message.id);
|
||||
});
|
||||
_activeTimers[message.id] = timer;
|
||||
}
|
||||
|
||||
Future<void> _handleRemove(CommonMessage commonMessage) async {
|
||||
_messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value)
|
||||
..remove(commonMessage);
|
||||
if (_bufferMessages.isEmpty) {
|
||||
_pushing = false;
|
||||
}
|
||||
void _removeMessage(String id) {
|
||||
_activeTimers.remove(id)?.cancel();
|
||||
final currentMessages = List<CommonMessage>.from(_messagesNotifier.value);
|
||||
currentMessages.removeWhere((msg) => msg.id == id);
|
||||
_messagesNotifier.value = currentMessages;
|
||||
_isDisplayingMessage = false;
|
||||
_processQueue();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,35 +95,47 @@ class MessageManagerState extends State<MessageManager> {
|
||||
: LayoutBuilder(
|
||||
key: Key(messages.last.id),
|
||||
builder: (_, constraints) {
|
||||
return Card(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12.0),
|
||||
return Dismissible(
|
||||
key: ValueKey(messages.last.id),
|
||||
onDismissed: (_) {
|
||||
_cancelMessage(messages.last.id);
|
||||
},
|
||||
child: Card(
|
||||
shape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(14),
|
||||
),
|
||||
),
|
||||
),
|
||||
elevation: 10,
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
child: Container(
|
||||
width: min(constraints.maxWidth, 500),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(messages.last.text)),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
_handleRemove(messages.last);
|
||||
},
|
||||
icon: Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
elevation: 10,
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
child: Container(
|
||||
width: min(constraints.maxWidth, 500),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
messages.last.text,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
IconButton(
|
||||
padding: EdgeInsets.all(2),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
_cancelMessage(messages.last.id);
|
||||
},
|
||||
icon: Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -13,10 +13,7 @@ import '../providers/state.dart';
|
||||
class ThemeManager extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const ThemeManager({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
const ThemeManager({super.key, required this.child});
|
||||
|
||||
Widget _buildSystemUi(Widget child) {
|
||||
if (!system.isAndroid) {
|
||||
@@ -85,23 +82,28 @@ class ThemeManager extends ConsumerWidget {
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(
|
||||
textScaleFactor,
|
||||
),
|
||||
textScaler: TextScaler.linear(textScaleFactor),
|
||||
padding: padding.copyWith(
|
||||
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();
|
||||
@@ -22,12 +20,18 @@ class _TileContainerState extends State<TileManager> with TileListener {
|
||||
|
||||
@override
|
||||
void onStart() {
|
||||
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,19 +271,17 @@ class AppIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.all(6),
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircleAvatar(
|
||||
foregroundImage: AssetImage('assets/images/icon.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
decoration: ShapeDecoration(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -1),
|
||||
child: Image.asset('assets/images/icon.png', width: 34, height: 34),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ abstract class AppState with _$AppState {
|
||||
@Default([]) List<Package> packages,
|
||||
@Default(0) int sortNum,
|
||||
required Size viewSize,
|
||||
@Default(0) double sideWidth,
|
||||
@Default({}) DelayMap delayMap,
|
||||
@Default([]) List<Group> groups,
|
||||
@Default(0) int checkIpNum,
|
||||
|
||||
@@ -445,6 +445,7 @@ abstract class AndroidState with _$AndroidState {
|
||||
required String currentProfileName,
|
||||
required String stopText,
|
||||
required bool onlyStatisticsProxy,
|
||||
required bool crashlytics,
|
||||
}) = _AndroidState;
|
||||
|
||||
factory AndroidState.fromJson(Map<String, Object?> json) =>
|
||||
|
||||
@@ -77,6 +77,8 @@ abstract class AppSettingProps with _$AppSettingProps {
|
||||
@Default(true) bool autoCheckUpdate,
|
||||
@Default(false) bool showLabel,
|
||||
@Default(false) bool disclaimerAccepted,
|
||||
@Default(false) bool crashlyticsTip,
|
||||
@Default(false) bool crashlytics,
|
||||
@Default(true) bool minimizeOnExit,
|
||||
@Default(false) bool hidden,
|
||||
@Default(false) bool developerMode,
|
||||
|
||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$AppState {
|
||||
|
||||
bool get isInit; bool get backBlock; PageLabel get pageLabel; List<Package> get packages; int get sortNum; Size get viewSize; DelayMap get delayMap; List<Group> get groups; int get checkIpNum; Brightness get brightness; int? get runTime; List<ExternalProvider> get providers; String? get localIp; FixedList<TrackerInfo> get requests; int get version; FixedList<Log> get logs; FixedList<Traffic> get traffics; Traffic get totalTraffic; bool get realTunEnable; bool get loading; SystemUiOverlayStyle get systemUiOverlayStyle; ProfileOverrideModel? get profileOverrideModel; Map<QueryTag, String> get queryMap; CoreStatus get coreStatus;
|
||||
bool get isInit; bool get backBlock; PageLabel get pageLabel; List<Package> get packages; int get sortNum; Size get viewSize; double get sideWidth; DelayMap get delayMap; List<Group> get groups; int get checkIpNum; Brightness get brightness; int? get runTime; List<ExternalProvider> get providers; String? get localIp; FixedList<TrackerInfo> get requests; int get version; FixedList<Log> get logs; FixedList<Traffic> get traffics; Traffic get totalTraffic; bool get realTunEnable; bool get loading; SystemUiOverlayStyle get systemUiOverlayStyle; ProfileOverrideModel? get profileOverrideModel; Map<QueryTag, String> get queryMap; CoreStatus get coreStatus;
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -25,16 +25,16 @@ $AppStateCopyWith<AppState> get copyWith => _$AppStateCopyWithImpl<AppState>(thi
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.backBlock, backBlock) || other.backBlock == backBlock)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other.packages, packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&const DeepCollectionEquality().equals(other.delayMap, delayMap)&&const DeepCollectionEquality().equals(other.groups, groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other.providers, providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.realTunEnable, realTunEnable) || other.realTunEnable == realTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.profileOverrideModel, profileOverrideModel) || other.profileOverrideModel == profileOverrideModel)&&const DeepCollectionEquality().equals(other.queryMap, queryMap)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.backBlock, backBlock) || other.backBlock == backBlock)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other.packages, packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&(identical(other.sideWidth, sideWidth) || other.sideWidth == sideWidth)&&const DeepCollectionEquality().equals(other.delayMap, delayMap)&&const DeepCollectionEquality().equals(other.groups, groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other.providers, providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.realTunEnable, realTunEnable) || other.realTunEnable == realTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.profileOverrideModel, profileOverrideModel) || other.profileOverrideModel == profileOverrideModel)&&const DeepCollectionEquality().equals(other.queryMap, queryMap)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,backBlock,pageLabel,const DeepCollectionEquality().hash(packages),sortNum,viewSize,const DeepCollectionEquality().hash(delayMap),const DeepCollectionEquality().hash(groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(providers),localIp,requests,version,logs,traffics,totalTraffic,realTunEnable,loading,systemUiOverlayStyle,profileOverrideModel,const DeepCollectionEquality().hash(queryMap),coreStatus]);
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,backBlock,pageLabel,const DeepCollectionEquality().hash(packages),sortNum,viewSize,sideWidth,const DeepCollectionEquality().hash(delayMap),const DeepCollectionEquality().hash(groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(providers),localIp,requests,version,logs,traffics,totalTraffic,realTunEnable,loading,systemUiOverlayStyle,profileOverrideModel,const DeepCollectionEquality().hash(queryMap),coreStatus]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState(isInit: $isInit, backBlock: $backBlock, pageLabel: $pageLabel, packages: $packages, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, realTunEnable: $realTunEnable, loading: $loading, systemUiOverlayStyle: $systemUiOverlayStyle, profileOverrideModel: $profileOverrideModel, queryMap: $queryMap, coreStatus: $coreStatus)';
|
||||
return 'AppState(isInit: $isInit, backBlock: $backBlock, pageLabel: $pageLabel, packages: $packages, sortNum: $sortNum, viewSize: $viewSize, sideWidth: $sideWidth, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, realTunEnable: $realTunEnable, loading: $loading, systemUiOverlayStyle: $systemUiOverlayStyle, profileOverrideModel: $profileOverrideModel, queryMap: $queryMap, coreStatus: $coreStatus)';
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ abstract mixin class $AppStateCopyWith<$Res> {
|
||||
factory $AppStateCopyWith(AppState value, $Res Function(AppState) _then) = _$AppStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus
|
||||
bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class _$AppStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isInit = null,Object? backBlock = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? realTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? profileOverrideModel = freezed,Object? queryMap = null,Object? coreStatus = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isInit = null,Object? backBlock = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? sideWidth = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? realTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? profileOverrideModel = freezed,Object? queryMap = null,Object? coreStatus = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
isInit: null == isInit ? _self.isInit : isInit // ignore: cast_nullable_to_non_nullable
|
||||
as bool,backBlock: null == backBlock ? _self.backBlock : backBlock // ignore: cast_nullable_to_non_nullable
|
||||
@@ -70,7 +70,8 @@ as bool,pageLabel: null == pageLabel ? _self.pageLabel : pageLabel // ignore: ca
|
||||
as PageLabel,packages: null == packages ? _self.packages : packages // ignore: cast_nullable_to_non_nullable
|
||||
as List<Package>,sortNum: null == sortNum ? _self.sortNum : sortNum // ignore: cast_nullable_to_non_nullable
|
||||
as int,viewSize: null == viewSize ? _self.viewSize : viewSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size,delayMap: null == delayMap ? _self.delayMap : delayMap // ignore: cast_nullable_to_non_nullable
|
||||
as Size,sideWidth: null == sideWidth ? _self.sideWidth : sideWidth // ignore: cast_nullable_to_non_nullable
|
||||
as double,delayMap: null == delayMap ? _self.delayMap : delayMap // ignore: cast_nullable_to_non_nullable
|
||||
as DelayMap,groups: null == groups ? _self.groups : groups // ignore: cast_nullable_to_non_nullable
|
||||
as List<Group>,checkIpNum: null == checkIpNum ? _self.checkIpNum : checkIpNum // ignore: cast_nullable_to_non_nullable
|
||||
as int,brightness: null == brightness ? _self.brightness : brightness // ignore: cast_nullable_to_non_nullable
|
||||
@@ -194,10 +195,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.realTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.profileOverrideModel,_that.queryMap,_that.coreStatus);case _:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.realTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.profileOverrideModel,_that.queryMap,_that.coreStatus);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -215,10 +216,10 @@ return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState():
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.realTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.profileOverrideModel,_that.queryMap,_that.coreStatus);case _:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.realTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.profileOverrideModel,_that.queryMap,_that.coreStatus);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -235,10 +236,10 @@ return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.realTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.profileOverrideModel,_that.queryMap,_that.coreStatus);case _:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.realTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.profileOverrideModel,_that.queryMap,_that.coreStatus);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -250,7 +251,7 @@ return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_tha
|
||||
|
||||
|
||||
class _AppState implements AppState {
|
||||
const _AppState({this.isInit = false, this.backBlock = false, this.pageLabel = PageLabel.dashboard, final List<Package> packages = const [], this.sortNum = 0, required this.viewSize, final DelayMap delayMap = const {}, final List<Group> groups = const [], this.checkIpNum = 0, required this.brightness, this.runTime, final List<ExternalProvider> providers = const [], this.localIp, required this.requests, required this.version, required this.logs, required this.traffics, required this.totalTraffic, this.realTunEnable = false, this.loading = false, required this.systemUiOverlayStyle, this.profileOverrideModel, final Map<QueryTag, String> queryMap = const {}, this.coreStatus = CoreStatus.connecting}): _packages = packages,_delayMap = delayMap,_groups = groups,_providers = providers,_queryMap = queryMap;
|
||||
const _AppState({this.isInit = false, this.backBlock = false, this.pageLabel = PageLabel.dashboard, final List<Package> packages = const [], this.sortNum = 0, required this.viewSize, this.sideWidth = 0, final DelayMap delayMap = const {}, final List<Group> groups = const [], this.checkIpNum = 0, required this.brightness, this.runTime, final List<ExternalProvider> providers = const [], this.localIp, required this.requests, required this.version, required this.logs, required this.traffics, required this.totalTraffic, this.realTunEnable = false, this.loading = false, required this.systemUiOverlayStyle, this.profileOverrideModel, final Map<QueryTag, String> queryMap = const {}, this.coreStatus = CoreStatus.connecting}): _packages = packages,_delayMap = delayMap,_groups = groups,_providers = providers,_queryMap = queryMap;
|
||||
|
||||
|
||||
@override@JsonKey() final bool isInit;
|
||||
@@ -265,6 +266,7 @@ class _AppState implements AppState {
|
||||
|
||||
@override@JsonKey() final int sortNum;
|
||||
@override final Size viewSize;
|
||||
@override@JsonKey() final double sideWidth;
|
||||
final DelayMap _delayMap;
|
||||
@override@JsonKey() DelayMap get delayMap {
|
||||
if (_delayMap is EqualUnmodifiableMapView) return _delayMap;
|
||||
@@ -318,16 +320,16 @@ _$AppStateCopyWith<_AppState> get copyWith => __$AppStateCopyWithImpl<_AppState>
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.backBlock, backBlock) || other.backBlock == backBlock)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other._packages, _packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&const DeepCollectionEquality().equals(other._delayMap, _delayMap)&&const DeepCollectionEquality().equals(other._groups, _groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other._providers, _providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.realTunEnable, realTunEnable) || other.realTunEnable == realTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.profileOverrideModel, profileOverrideModel) || other.profileOverrideModel == profileOverrideModel)&&const DeepCollectionEquality().equals(other._queryMap, _queryMap)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.backBlock, backBlock) || other.backBlock == backBlock)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other._packages, _packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&(identical(other.sideWidth, sideWidth) || other.sideWidth == sideWidth)&&const DeepCollectionEquality().equals(other._delayMap, _delayMap)&&const DeepCollectionEquality().equals(other._groups, _groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other._providers, _providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.realTunEnable, realTunEnable) || other.realTunEnable == realTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.profileOverrideModel, profileOverrideModel) || other.profileOverrideModel == profileOverrideModel)&&const DeepCollectionEquality().equals(other._queryMap, _queryMap)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,backBlock,pageLabel,const DeepCollectionEquality().hash(_packages),sortNum,viewSize,const DeepCollectionEquality().hash(_delayMap),const DeepCollectionEquality().hash(_groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(_providers),localIp,requests,version,logs,traffics,totalTraffic,realTunEnable,loading,systemUiOverlayStyle,profileOverrideModel,const DeepCollectionEquality().hash(_queryMap),coreStatus]);
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,backBlock,pageLabel,const DeepCollectionEquality().hash(_packages),sortNum,viewSize,sideWidth,const DeepCollectionEquality().hash(_delayMap),const DeepCollectionEquality().hash(_groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(_providers),localIp,requests,version,logs,traffics,totalTraffic,realTunEnable,loading,systemUiOverlayStyle,profileOverrideModel,const DeepCollectionEquality().hash(_queryMap),coreStatus]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppState(isInit: $isInit, backBlock: $backBlock, pageLabel: $pageLabel, packages: $packages, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, realTunEnable: $realTunEnable, loading: $loading, systemUiOverlayStyle: $systemUiOverlayStyle, profileOverrideModel: $profileOverrideModel, queryMap: $queryMap, coreStatus: $coreStatus)';
|
||||
return 'AppState(isInit: $isInit, backBlock: $backBlock, pageLabel: $pageLabel, packages: $packages, sortNum: $sortNum, viewSize: $viewSize, sideWidth: $sideWidth, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, realTunEnable: $realTunEnable, loading: $loading, systemUiOverlayStyle: $systemUiOverlayStyle, profileOverrideModel: $profileOverrideModel, queryMap: $queryMap, coreStatus: $coreStatus)';
|
||||
}
|
||||
|
||||
|
||||
@@ -338,7 +340,7 @@ abstract mixin class _$AppStateCopyWith<$Res> implements $AppStateCopyWith<$Res>
|
||||
factory _$AppStateCopyWith(_AppState value, $Res Function(_AppState) _then) = __$AppStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus
|
||||
bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, bool realTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, ProfileOverrideModel? profileOverrideModel, Map<QueryTag, String> queryMap, CoreStatus coreStatus
|
||||
});
|
||||
|
||||
|
||||
@@ -355,7 +357,7 @@ class __$AppStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isInit = null,Object? backBlock = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? realTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? profileOverrideModel = freezed,Object? queryMap = null,Object? coreStatus = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isInit = null,Object? backBlock = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? sideWidth = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? realTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? profileOverrideModel = freezed,Object? queryMap = null,Object? coreStatus = null,}) {
|
||||
return _then(_AppState(
|
||||
isInit: null == isInit ? _self.isInit : isInit // ignore: cast_nullable_to_non_nullable
|
||||
as bool,backBlock: null == backBlock ? _self.backBlock : backBlock // ignore: cast_nullable_to_non_nullable
|
||||
@@ -363,7 +365,8 @@ as bool,pageLabel: null == pageLabel ? _self.pageLabel : pageLabel // ignore: ca
|
||||
as PageLabel,packages: null == packages ? _self._packages : packages // ignore: cast_nullable_to_non_nullable
|
||||
as List<Package>,sortNum: null == sortNum ? _self.sortNum : sortNum // ignore: cast_nullable_to_non_nullable
|
||||
as int,viewSize: null == viewSize ? _self.viewSize : viewSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size,delayMap: null == delayMap ? _self._delayMap : delayMap // ignore: cast_nullable_to_non_nullable
|
||||
as Size,sideWidth: null == sideWidth ? _self.sideWidth : sideWidth // ignore: cast_nullable_to_non_nullable
|
||||
as double,delayMap: null == delayMap ? _self._delayMap : delayMap // ignore: cast_nullable_to_non_nullable
|
||||
as DelayMap,groups: null == groups ? _self._groups : groups // ignore: cast_nullable_to_non_nullable
|
||||
as List<Group>,checkIpNum: null == checkIpNum ? _self.checkIpNum : checkIpNum // ignore: cast_nullable_to_non_nullable
|
||||
as int,brightness: null == brightness ? _self.brightness : brightness // ignore: cast_nullable_to_non_nullable
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user