Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
a0183414fe Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-21 21:58:32 +08:00
107 changed files with 47000 additions and 37051 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package com.follow.clash
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.follow.clash.common.GlobalState
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
@@ -31,6 +32,9 @@ class MainActivity : FlutterActivity(),
}
override fun onDestroy() {
GlobalState.launch {
Service.setEventListener(null)
}
State.flutterEngine = null
super.onDestroy()
}

View File

@@ -1,13 +1,18 @@
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 kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object Service {
private val delegate by lazy {
@@ -34,15 +39,46 @@ object Service {
delegate.unbind()
}
suspend fun invokeAction(
data: String, cb: (result: ByteArray?, isSuccess: Boolean) -> Unit
): Result<Unit> {
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) {
cb(result, isSuccess)
it.invokeAction(
data, object : ICallbackInterface.Stub() {
override fun onResult(result: ByteArray?, isSuccess: Boolean) {
res.add(result ?: byteArrayOf())
if (isSuccess) {
cb(res.formatString())
}
}
})
}
}
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
}
})
)
}
}
@@ -54,23 +90,54 @@ object Service {
}
}
suspend fun setMessageCallback(
cb: (result: String?) -> Unit
suspend fun setCrashlytics(
enable: Boolean
): Result<Unit> {
return delegate.useService {
it.setMessageCallback(object : IMessageInterface.Stub() {
override fun onResult(result: String?) {
cb(result)
}
})
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
}
}

View File

@@ -26,6 +26,7 @@ object State {
var runTime: Long = 0
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
var flutterEngine: FlutterEngine? = null
var serviceFlutterEngine: FlutterEngine? = null
@@ -51,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,8 +134,7 @@ object State {
return@launch
}
appPlugin?.prepare(options.enable) {
runTime = System.currentTimeMillis()
Service.startService(options, true)
runTime = Service.startService(options, runTime)
runStateFlow.tryEmit(RunState.START)
}
}
@@ -135,14 +149,13 @@ object State {
return@launch
}
runStateFlow.tryEmit(RunState.PENDING)
Service.stopService()
runTime = Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
runTime = 0
}
destroyServiceEngine()
}
}
}

View File

@@ -31,6 +31,7 @@ class TileService : TileService() {
scope?.cancel()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
scope?.launch {
State.handleSyncState()
State.runStateFlow.collect {
updateTile(it)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -149,7 +149,7 @@ fun Context.receiveBroadcastFlow(
inline fun <reified T : IBinder> Context.bindServiceFlow(
intent: Intent,
flags: Int = Context.BIND_AUTO_CREATE,
maxRetries: Int = 5,
maxRetries: Int = 10,
retryDelayMillis: Long = 200L
): Flow<Pair<IBinder?, String>> = callbackFlow {
val connection = object : ServiceConnection {
@@ -186,6 +186,7 @@ inline fun <reified T : IBinder> Context.bindServiceFlow(
awaitClose {
Handler(Looper.getMainLooper()).post {
unbindService(connection)
trySend(Pair(null, ""))
}
}
}.retryWhen { cause, attempt ->

View File

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

View File

@@ -44,13 +44,16 @@ class ServiceDelegate<T>(
job = null
_serviceState.value = null
job = launch {
GlobalState.application.bindServiceFlow<IBinder>(intent).collect { handleBind(it) }
runCatching {
GlobalState.application.bindServiceFlow<IBinder>(intent)
.collect { handleBind(it) }
}
}
}
}
suspend inline fun <R> useService(
timeoutMillis: Long = 5000, crossinline block: (T) -> R
timeoutMillis: Long = 5000, crossinline block: suspend (T) -> R
): Result<R> {
return runCatching {
withTimeout(timeoutMillis) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
// IMessageInterface.aidl
package com.follow.clash.service;
interface IMessageInterface {
void onResult(String result);
}

View File

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

View File

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

View File

@@ -29,6 +29,11 @@ class CommonService : Service(), IBaseService,
handleCreate()
}
override fun onDestroy() {
handleDestroy()
super.onDestroy()
}
override fun onLowMemory() {
Core.forceGC()
super.onLowMemory()

View File

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

View File

@@ -3,56 +3,71 @@ 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()
runLock.withLock {
delegate?.useService { service ->
service.stop()
delegate?.unbind()
}
State.runTime = 0
result.onResult(0)
}
}
}
private fun handleServiceDisconnected(message: String) {
GlobalState.log("Background service disconnected: $message")
intent = null
delegate = null
}
private fun handleStartService() {
private fun handleStartService(runTime: Long, result: IResultInterface) {
launch {
val nextIntent = when (State.options?.enable == true) {
true -> VpnService::class.intent
false -> CommonService::class.intent
}
if (intent != nextIntent) {
delegate?.unbind()
delegate = ServiceDelegate(nextIntent, ::handleServiceDisconnected) { binder ->
when (binder) {
is VpnService.LocalBinder -> binder.getService()
is CommonService.LocalBinder -> binder.getService()
else -> throw IllegalArgumentException("Invalid binder type")
}
runLock.withLock {
val nextIntent = when (State.options?.enable == true) {
true -> VpnService::class.intent
false -> CommonService::class.intent
}
intent = nextIntent
delegate?.bind()
}
delegate?.useService { service ->
service.start()
if (intent != nextIntent) {
delegate?.unbind()
delegate = ServiceDelegate(nextIntent, ::handleServiceDisconnected) { binder ->
when (binder) {
is VpnService.LocalBinder -> binder.getService()
is CommonService.LocalBinder -> binder.getService()
else -> throw IllegalArgumentException("Invalid binder type")
}
}
intent = nextIntent
delegate?.bind()
}
delegate?.useService { service ->
service.start()
}
State.runTime = when (runTime != 0L) {
true -> runTime
false -> System.currentTimeMillis()
}
result.onResult(State.runTime)
}
}
}
@@ -60,10 +75,12 @@ class RemoteService : Service(),
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)
}
}
}
}
@@ -73,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()
}
}

View File

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

View File

@@ -11,8 +11,7 @@ import android.os.RemoteException
import android.util.Log
import androidx.core.content.getSystemService
import com.follow.clash.common.AccessControlMode
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.sendBroadcast
import com.follow.clash.common.GlobalState
import com.follow.clash.core.Core
import com.follow.clash.service.models.VpnOptions
import com.follow.clash.service.models.getIpv4RouteAddress
@@ -44,6 +43,11 @@ class VpnService : SystemVpnService(), IBaseService,
handleCreate()
}
override fun onDestroy() {
handleDestroy()
super.onDestroy()
}
private val connectivity by lazy {
getSystemService<ConnectivityManager>()
}
@@ -107,11 +111,13 @@ class VpnService : SystemVpnService(), IBaseService,
try {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
BroadcastAction.STOP.sendBroadcast()
GlobalState.log("VpnService disconnected")
handleDestroy()
}
return isSuccess
} catch (e: RemoteException) {
throw e
GlobalState.log("VpnService onTransact $e")
return false
}
}
}

View File

@@ -90,12 +90,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 +106,6 @@ class NotificationModule(private val service: Service) : Module() {
with(notificationBuilder) {
setContentTitle(params.title)
setContentText(params.contentText)
setPriority(NotificationCompat.PRIORITY_HIGH)
clearActions()
addAction(
0, params.stopText, QuickAction.STOP.quickIntent.toPendingIntent

View File

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

View File

@@ -429,5 +429,9 @@
"restartCoreTip": "コアを再起動してもよろしいですか?",
"forceRestartCoreTip": "コアを強制再起動してもよろしいですか?",
"dnsHijacking": "DNSハイジャッキング",
"coreStatus": "コアステータス"
"coreStatus": "コアステータス",
"dataCollectionTip": "データ収集説明",
"dataCollectionContent": "本アプリはFirebase Crashlyticsを使用してクラッシュ情報を収集し、アプリの安定性を向上させます。\n収集されるデータにはデバイス情報とクラッシュ詳細が含まれますが、個人の機密データは含まれません。\n設定でこの機能を無効にすることができます。",
"crashlytics": "クラッシュ分析",
"crashlyticsTip": "有効にすると、アプリがクラッシュした際に機密情報を含まないクラッシュログを自動的にアップロードします"
}

View File

@@ -429,5 +429,9 @@
"restartCoreTip": "Вы уверены, что хотите перезапустить ядро?",
"forceRestartCoreTip": "Вы уверены, что хотите принудительно перезапустить ядро?",
"dnsHijacking": "DNS-перехват",
"coreStatus": "Основной статус"
"coreStatus": "Основной статус",
"dataCollectionTip": "Уведомление о сборе данных",
"dataCollectionContent": "Это приложение использует Firebase Crashlytics для сбора информации о сбоях nhằm улучшения стабильности приложения.\nСобираемые данные включают информацию об устройстве и подробности о сбоях, но не содержат персональных конфиденциальных данных.\nВы можете отключить эту функцию в настройках.",
"crashlytics": "Анализ сбоев",
"crashlyticsTip": "При включении автоматически загружает журналы сбоев без конфиденциальной информации, когда приложение выходит из строя"
}

View File

@@ -429,5 +429,9 @@
"restartCoreTip": "您确定要重启核心吗?",
"forceRestartCoreTip": "您确定要强制重启核心吗?",
"dnsHijacking": "DNS劫持",
"coreStatus": "核心状态"
"coreStatus": "核心状态",
"dataCollectionTip": "数据收集说明",
"dataCollectionContent": "本应用使用 Firebase Crashlytics 收集崩溃信息以改进应用稳定性。\n收集的数据包括设备信息和崩溃详情不包含个人敏感数据。\n您可以在设置中关闭此功能。",
"crashlytics": "崩溃分析",
"crashlyticsTip": "开启后,应用崩溃时自动上传不包含敏感信息的崩溃日志"
}

Binary file not shown.

BIN
assets/data/GEOIP.metadb Normal file

Binary file not shown.

45523
assets/data/GEOSITE.dat Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,6 +1,6 @@
module core
go 1.20
go 1.25
replace github.com/metacubex/mihomo => ./Clash.Meta

View File

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

View File

@@ -27,7 +27,7 @@ import (
"unsafe"
)
var messageCallback unsafe.Pointer
var eventListener unsafe.Pointer
type TunHandler struct {
listener *sing_tun.Listener
@@ -164,7 +164,7 @@ func (result ActionResult) send() {
}
invokeResult(result.callback, string(data))
if result.Method != messageMethod {
defer releaseObject(result.callback)
releaseObject(result.callback)
}
}
@@ -202,12 +202,12 @@ func startTUN(callback unsafe.Pointer, fd C.int, stackChar, addressChar, dnsChar
return true
}
//export setMessageCallback
func setMessageCallback(callback unsafe.Pointer) {
if messageCallback != nil {
releaseObject(messageCallback)
//export setEventListener
func setEventListener(listener unsafe.Pointer) {
if eventListener != nil || listener == nil {
releaseObject(eventListener)
}
messageCallback = callback
eventListener = listener
}
//export getTotalTraffic
@@ -225,12 +225,12 @@ func getTraffic(onlyStatisticsProxy bool) *C.char {
}
func sendMessage(message Message) {
if messageCallback == nil {
if eventListener == nil {
return
}
result := ActionResult{
Method: messageMethod,
callback: messageCallback,
callback: eventListener,
Data: message,
}
result.send()

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
export 'android.dart';
export 'app_localizations.dart';
export 'cache.dart';
export 'color.dart';

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
@@ -12,14 +13,14 @@ class CommonPrint {
return _instance!;
}
void log(String? text) {
void log(String? text, {LogLevel logLevel = LogLevel.info}) {
final payload = '[APP] $text';
debugPrint(payload);
if (!globalState.isInit) {
return;
}
globalState.appController.addLog(
Log.app(payload),
Log.app(payload).copyWith(logLevel: logLevel),
);
}
}

View File

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

View File

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

View File

@@ -37,7 +37,7 @@ class System {
'macos' => (deviceInfo as MacOsDeviceInfo).majorVersion,
'android' => (deviceInfo as AndroidDeviceInfo).version.sdkInt,
'windows' => (deviceInfo as WindowsDeviceInfo).majorVersion,
String() => 0
String() => 0,
};
}
@@ -104,7 +104,7 @@ class System {
);
final arguments = [
'-c',
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"',
];
final result = await Process.run(shell, arguments);
if (result.exitCode != 0) {
@@ -148,21 +148,25 @@ class Windows {
final argumentsPtr = arguments.toNativeUtf16();
final operationPtr = 'runas'.toNativeUtf16();
final shellExecute = _shell32.lookupFunction<
Int32 Function(
final shellExecute = _shell32
.lookupFunction<
Int32 Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
Int32 nShowCmd),
int Function(
Int32 nShowCmd,
),
int Function(
Pointer<Utf16> hwnd,
Pointer<Utf16> lpOperation,
Pointer<Utf16> lpFile,
Pointer<Utf16> lpParameters,
Pointer<Utf16> lpDirectory,
int nShowCmd)>('ShellExecuteW');
int nShowCmd,
)
>('ShellExecuteW');
final result = shellExecute(
nullptr,
@@ -177,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',
]);
}
}

View File

@@ -8,7 +8,6 @@ import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/core/core.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/dialog.dart';
@@ -354,7 +353,7 @@ class AppController {
try {
await updateProfile(profile);
} catch (e) {
commonPrint.log(e.toString());
commonPrint.log(e.toString(), logLevel: LogLevel.warning);
}
}
}
@@ -530,6 +529,7 @@ class AppController {
FlutterError.onError = (details) {
commonPrint.log(
'exception: ${details.exception} stack: ${details.stack}',
logLevel: LogLevel.warning,
);
};
updateTray(true);
@@ -543,8 +543,8 @@ class AppController {
}
await _handlePreference();
await _handlerDisclaimer();
await _showCrashlyticsTip();
await _connectCore();
await service?.syncAndroidState(globalState.getAndroidState());
await _initCore();
await _initStatus();
_ref.read(initProvider.notifier).value = true;
@@ -552,7 +552,12 @@ class AppController {
Future<void> _connectCore() async {
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
final message = await coreController.preload();
final result = await Future.wait([
coreController.preload(),
if (!globalState.isService) Future.delayed(Duration(milliseconds: 300)),
]);
final String message = result[0];
await Future.delayed(commonDuration);
if (message.isNotEmpty) {
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
if (context.mounted) {
@@ -620,7 +625,6 @@ class AppController {
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
context: context,
dismissible: false,
child: CommonDialog(
title: appLocalizations.disclaimer,
@@ -633,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;
}
@@ -941,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 {

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ class CoreService extends CoreHandlerInterface {
}
},
(error, stack) async {
commonPrint.log('Service error: $error');
commonPrint.log('Service error: $error', logLevel: LogLevel.warning);
},
);
}
@@ -97,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
@@ -126,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();
}
}
@@ -179,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;

View File

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

View File

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

View File

@@ -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(
"Сервер имен по умолчанию",

View File

@@ -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服务器"),

View File

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

View File

@@ -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,9 +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 coreController.preload();
await service?.syncAndroidState(globalState.getAndroidState());
tile?.addListener(
_TileListenerWithService(
onStop: () async {

View File

@@ -186,74 +186,68 @@ class AppSidebarContainer extends ConsumerWidget {
children: [
_buildBackground(
context: context,
child: Stack(
alignment: Alignment.topRight,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (system.isMacOS) SizedBox(height: 22),
SizedBox(height: 10),
if (!system.isMacOS) ...[
ClipRect(
child: Padding(
padding: EdgeInsets.only(left: 20),
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)),
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,
),
)
.toList(),
onDestinationSelected: (index) {
globalState.appController.toPage(
navigationItems[index].label,
);
},
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
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),
Padding(
padding: EdgeInsets.only(left: 20),
child: IconButton(
const SizedBox(height: 16),
IconButton(
onPressed: () {
ref
.read(appSettingProvider.notifier)
@@ -267,12 +261,12 @@ class AppSidebarContainer extends ConsumerWidget {
color: context.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(height: 16),
],
),
_buildLoading(),
],
const SizedBox(height: 16),
],
),
_buildLoading(),
],
),
),
),
Expanded(

View File

@@ -95,8 +95,8 @@ class _CoreContainerState extends ConsumerState<CoreManager>
Future<void> onCrash(String message) async {
if (!globalState.isUserDisconnected) {
context.showNotifier(message);
globalState.isUserDisconnected = false;
}
globalState.isUserDisconnected = false;
if (ref.read(coreStatusProvider) != CoreStatus.connected) {
return;
}

View File

@@ -101,7 +101,7 @@ class MessageManagerState extends State<MessageManager> {
_cancelMessage(messages.last.id);
},
child: Card(
shape: const RoundedRectangleBorder(
shape: const RoundedSuperellipseBorder(
borderRadius: BorderRadius.all(
Radius.circular(14),
),
@@ -112,7 +112,7 @@ class MessageManagerState extends State<MessageManager> {
width: min(constraints.maxWidth, 500),
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
vertical: 8,
),
child: Row(
mainAxisAlignment:
@@ -126,11 +126,13 @@ class MessageManagerState extends State<MessageManager> {
),
),
SizedBox(width: 16),
TextButton(
IconButton(
padding: EdgeInsets.all(2),
visualDensity: VisualDensity.compact,
onPressed: () {
_cancelMessage(messages.last.id);
},
child: Text(appLocalizations.cancel),
icon: Icon(Icons.close),
),
],
),

View File

@@ -87,13 +87,23 @@ class ThemeManager extends ConsumerWidget {
top: padding.top > height * 0.3 ? 20.0 : padding.top,
),
),
child: LayoutBuilder(
builder: (_, container) {
globalState.appController.updateViewSize(
Size(container.maxWidth, container.maxHeight),
);
return _buildSystemUi(child);
},
child: Theme(
data: Theme.of(context).copyWith(
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
.copyWith(
shape: const RoundedSuperellipseBorder(
borderRadius: BorderRadius.all(Radius.circular(16.0)),
),
),
),
child: LayoutBuilder(
builder: (_, container) {
globalState.appController.updateViewSize(
Size(container.maxWidth, container.maxHeight),
);
return _buildSystemUi(child);
},
),
),
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4973,7 +4973,7 @@ as Validator?,
/// @nodoc
mixin _$AndroidState {
String get currentProfileName; String get stopText; bool get onlyStatisticsProxy;
String get currentProfileName; String get stopText; bool get onlyStatisticsProxy; bool get crashlytics;
/// Create a copy of AndroidState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -4986,16 +4986,16 @@ $AndroidStateCopyWith<AndroidState> get copyWith => _$AndroidStateCopyWithImpl<A
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AndroidState&&(identical(other.currentProfileName, currentProfileName) || other.currentProfileName == currentProfileName)&&(identical(other.stopText, stopText) || other.stopText == stopText)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AndroidState&&(identical(other.currentProfileName, currentProfileName) || other.currentProfileName == currentProfileName)&&(identical(other.stopText, stopText) || other.stopText == stopText)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,currentProfileName,stopText,onlyStatisticsProxy);
int get hashCode => Object.hash(runtimeType,currentProfileName,stopText,onlyStatisticsProxy,crashlytics);
@override
String toString() {
return 'AndroidState(currentProfileName: $currentProfileName, stopText: $stopText, onlyStatisticsProxy: $onlyStatisticsProxy)';
return 'AndroidState(currentProfileName: $currentProfileName, stopText: $stopText, onlyStatisticsProxy: $onlyStatisticsProxy, crashlytics: $crashlytics)';
}
@@ -5006,7 +5006,7 @@ abstract mixin class $AndroidStateCopyWith<$Res> {
factory $AndroidStateCopyWith(AndroidState value, $Res Function(AndroidState) _then) = _$AndroidStateCopyWithImpl;
@useResult
$Res call({
String currentProfileName, String stopText, bool onlyStatisticsProxy
String currentProfileName, String stopText, bool onlyStatisticsProxy, bool crashlytics
});
@@ -5023,11 +5023,12 @@ class _$AndroidStateCopyWithImpl<$Res>
/// Create a copy of AndroidState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? currentProfileName = null,Object? stopText = null,Object? onlyStatisticsProxy = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? currentProfileName = null,Object? stopText = null,Object? onlyStatisticsProxy = null,Object? crashlytics = null,}) {
return _then(_self.copyWith(
currentProfileName: null == currentProfileName ? _self.currentProfileName : currentProfileName // ignore: cast_nullable_to_non_nullable
as String,stopText: null == stopText ? _self.stopText : stopText // ignore: cast_nullable_to_non_nullable
as String,onlyStatisticsProxy: null == onlyStatisticsProxy ? _self.onlyStatisticsProxy : onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable
as bool,crashlytics: null == crashlytics ? _self.crashlytics : crashlytics // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@@ -5113,10 +5114,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String currentProfileName, String stopText, bool onlyStatisticsProxy)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String currentProfileName, String stopText, bool onlyStatisticsProxy, bool crashlytics)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AndroidState() when $default != null:
return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProxy);case _:
return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProxy,_that.crashlytics);case _:
return orElse();
}
@@ -5134,10 +5135,10 @@ return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProx
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String currentProfileName, String stopText, bool onlyStatisticsProxy) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String currentProfileName, String stopText, bool onlyStatisticsProxy, bool crashlytics) $default,) {final _that = this;
switch (_that) {
case _AndroidState():
return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProxy);case _:
return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProxy,_that.crashlytics);case _:
throw StateError('Unexpected subclass');
}
@@ -5154,10 +5155,10 @@ return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProx
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String currentProfileName, String stopText, bool onlyStatisticsProxy)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String currentProfileName, String stopText, bool onlyStatisticsProxy, bool crashlytics)? $default,) {final _that = this;
switch (_that) {
case _AndroidState() when $default != null:
return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProxy);case _:
return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProxy,_that.crashlytics);case _:
return null;
}
@@ -5169,12 +5170,13 @@ return $default(_that.currentProfileName,_that.stopText,_that.onlyStatisticsProx
@JsonSerializable()
class _AndroidState implements AndroidState {
const _AndroidState({required this.currentProfileName, required this.stopText, required this.onlyStatisticsProxy});
const _AndroidState({required this.currentProfileName, required this.stopText, required this.onlyStatisticsProxy, required this.crashlytics});
factory _AndroidState.fromJson(Map<String, dynamic> json) => _$AndroidStateFromJson(json);
@override final String currentProfileName;
@override final String stopText;
@override final bool onlyStatisticsProxy;
@override final bool crashlytics;
/// Create a copy of AndroidState
/// with the given fields replaced by the non-null parameter values.
@@ -5189,16 +5191,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AndroidState&&(identical(other.currentProfileName, currentProfileName) || other.currentProfileName == currentProfileName)&&(identical(other.stopText, stopText) || other.stopText == stopText)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AndroidState&&(identical(other.currentProfileName, currentProfileName) || other.currentProfileName == currentProfileName)&&(identical(other.stopText, stopText) || other.stopText == stopText)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,currentProfileName,stopText,onlyStatisticsProxy);
int get hashCode => Object.hash(runtimeType,currentProfileName,stopText,onlyStatisticsProxy,crashlytics);
@override
String toString() {
return 'AndroidState(currentProfileName: $currentProfileName, stopText: $stopText, onlyStatisticsProxy: $onlyStatisticsProxy)';
return 'AndroidState(currentProfileName: $currentProfileName, stopText: $stopText, onlyStatisticsProxy: $onlyStatisticsProxy, crashlytics: $crashlytics)';
}
@@ -5209,7 +5211,7 @@ abstract mixin class _$AndroidStateCopyWith<$Res> implements $AndroidStateCopyWi
factory _$AndroidStateCopyWith(_AndroidState value, $Res Function(_AndroidState) _then) = __$AndroidStateCopyWithImpl;
@override @useResult
$Res call({
String currentProfileName, String stopText, bool onlyStatisticsProxy
String currentProfileName, String stopText, bool onlyStatisticsProxy, bool crashlytics
});
@@ -5226,11 +5228,12 @@ class __$AndroidStateCopyWithImpl<$Res>
/// Create a copy of AndroidState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? currentProfileName = null,Object? stopText = null,Object? onlyStatisticsProxy = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? currentProfileName = null,Object? stopText = null,Object? onlyStatisticsProxy = null,Object? crashlytics = null,}) {
return _then(_AndroidState(
currentProfileName: null == currentProfileName ? _self.currentProfileName : currentProfileName // ignore: cast_nullable_to_non_nullable
as String,stopText: null == stopText ? _self.stopText : stopText // ignore: cast_nullable_to_non_nullable
as String,onlyStatisticsProxy: null == onlyStatisticsProxy ? _self.onlyStatisticsProxy : onlyStatisticsProxy // ignore: cast_nullable_to_non_nullable
as bool,crashlytics: null == crashlytics ? _self.crashlytics : crashlytics // ignore: cast_nullable_to_non_nullable
as bool,
));
}

View File

@@ -244,6 +244,7 @@ _AndroidState _$AndroidStateFromJson(Map<String, dynamic> json) =>
currentProfileName: json['currentProfileName'] as String,
stopText: json['stopText'] as String,
onlyStatisticsProxy: json['onlyStatisticsProxy'] as bool,
crashlytics: json['crashlytics'] as bool,
);
Map<String, dynamic> _$AndroidStateToJson(_AndroidState instance) =>
@@ -251,6 +252,7 @@ Map<String, dynamic> _$AndroidStateToJson(_AndroidState instance) =>
'currentProfileName': instance.currentProfileName,
'stopText': instance.stopText,
'onlyStatisticsProxy': instance.onlyStatisticsProxy,
'crashlytics': instance.crashlytics,
};
_Script _$ScriptFromJson(Map<String, dynamic> json) => _Script(

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppSettingProps {
String? get locale;@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> get dashboardWidgets; bool get onlyStatisticsProxy; bool get autoLaunch; bool get silentLaunch; bool get autoRun; bool get openLogs; bool get closeConnections; String get testUrl; bool get isAnimateToPage; bool get autoCheckUpdate; bool get showLabel; bool get disclaimerAccepted; bool get minimizeOnExit; bool get hidden; bool get developerMode; RecoveryStrategy get recoveryStrategy;
String? get locale;@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> get dashboardWidgets; bool get onlyStatisticsProxy; bool get autoLaunch; bool get silentLaunch; bool get autoRun; bool get openLogs; bool get closeConnections; String get testUrl; bool get isAnimateToPage; bool get autoCheckUpdate; bool get showLabel; bool get disclaimerAccepted; bool get crashlyticsTip; bool get crashlytics; bool get minimizeOnExit; bool get hidden; bool get developerMode; RecoveryStrategy get recoveryStrategy;
/// Create a copy of AppSettingProps
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $AppSettingPropsCopyWith<AppSettingProps> get copyWith => _$AppSettingPropsCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other.dashboardWidgets, dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.recoveryStrategy, recoveryStrategy) || other.recoveryStrategy == recoveryStrategy));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other.dashboardWidgets, dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.crashlyticsTip, crashlyticsTip) || other.crashlyticsTip == crashlyticsTip)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.recoveryStrategy, recoveryStrategy) || other.recoveryStrategy == recoveryStrategy));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,locale,const DeepCollectionEquality().hash(dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,minimizeOnExit,hidden,developerMode,recoveryStrategy);
int get hashCode => Object.hashAll([runtimeType,locale,const DeepCollectionEquality().hash(dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,crashlyticsTip,crashlytics,minimizeOnExit,hidden,developerMode,recoveryStrategy]);
@override
String toString() {
return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, recoveryStrategy: $recoveryStrategy)';
return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, crashlyticsTip: $crashlyticsTip, crashlytics: $crashlytics, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, recoveryStrategy: $recoveryStrategy)';
}
@@ -48,7 +48,7 @@ abstract mixin class $AppSettingPropsCopyWith<$Res> {
factory $AppSettingPropsCopyWith(AppSettingProps value, $Res Function(AppSettingProps) _then) = _$AppSettingPropsCopyWithImpl;
@useResult
$Res call({
String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy
String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy
});
@@ -65,7 +65,7 @@ class _$AppSettingPropsCopyWithImpl<$Res>
/// Create a copy of AppSettingProps
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? recoveryStrategy = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? crashlyticsTip = null,Object? crashlytics = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? recoveryStrategy = null,}) {
return _then(_self.copyWith(
locale: freezed == locale ? _self.locale : locale // ignore: cast_nullable_to_non_nullable
as String?,dashboardWidgets: null == dashboardWidgets ? _self.dashboardWidgets : dashboardWidgets // ignore: cast_nullable_to_non_nullable
@@ -80,6 +80,8 @@ as String,isAnimateToPage: null == isAnimateToPage ? _self.isAnimateToPage : isA
as bool,autoCheckUpdate: null == autoCheckUpdate ? _self.autoCheckUpdate : autoCheckUpdate // ignore: cast_nullable_to_non_nullable
as bool,showLabel: null == showLabel ? _self.showLabel : showLabel // ignore: cast_nullable_to_non_nullable
as bool,disclaimerAccepted: null == disclaimerAccepted ? _self.disclaimerAccepted : disclaimerAccepted // ignore: cast_nullable_to_non_nullable
as bool,crashlyticsTip: null == crashlyticsTip ? _self.crashlyticsTip : crashlyticsTip // ignore: cast_nullable_to_non_nullable
as bool,crashlytics: null == crashlytics ? _self.crashlytics : crashlytics // ignore: cast_nullable_to_non_nullable
as bool,minimizeOnExit: null == minimizeOnExit ? _self.minimizeOnExit : minimizeOnExit // ignore: cast_nullable_to_non_nullable
as bool,hidden: null == hidden ? _self.hidden : hidden // ignore: cast_nullable_to_non_nullable
as bool,developerMode: null == developerMode ? _self.developerMode : developerMode // ignore: cast_nullable_to_non_nullable
@@ -169,10 +171,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppSettingProps() when $default != null:
return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.recoveryStrategy);case _:
return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.recoveryStrategy);case _:
return orElse();
}
@@ -190,10 +192,10 @@ return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_t
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy) $default,) {final _that = this;
switch (_that) {
case _AppSettingProps():
return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.recoveryStrategy);case _:
return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.recoveryStrategy);case _:
throw StateError('Unexpected subclass');
}
@@ -210,10 +212,10 @@ return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_t
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy)? $default,) {final _that = this;
switch (_that) {
case _AppSettingProps() when $default != null:
return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.recoveryStrategy);case _:
return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_that.autoLaunch,_that.silentLaunch,_that.autoRun,_that.openLogs,_that.closeConnections,_that.testUrl,_that.isAnimateToPage,_that.autoCheckUpdate,_that.showLabel,_that.disclaimerAccepted,_that.crashlyticsTip,_that.crashlytics,_that.minimizeOnExit,_that.hidden,_that.developerMode,_that.recoveryStrategy);case _:
return null;
}
@@ -225,7 +227,7 @@ return $default(_that.locale,_that.dashboardWidgets,_that.onlyStatisticsProxy,_t
@JsonSerializable()
class _AppSettingProps implements AppSettingProps {
const _AppSettingProps({this.locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) final List<DashboardWidget> dashboardWidgets = defaultDashboardWidgets, this.onlyStatisticsProxy = false, this.autoLaunch = false, this.silentLaunch = false, this.autoRun = false, this.openLogs = false, this.closeConnections = true, this.testUrl = defaultTestUrl, this.isAnimateToPage = true, this.autoCheckUpdate = true, this.showLabel = false, this.disclaimerAccepted = false, this.minimizeOnExit = true, this.hidden = false, this.developerMode = false, this.recoveryStrategy = RecoveryStrategy.compatible}): _dashboardWidgets = dashboardWidgets;
const _AppSettingProps({this.locale, @JsonKey(fromJson: dashboardWidgetsSafeFormJson) final List<DashboardWidget> dashboardWidgets = defaultDashboardWidgets, this.onlyStatisticsProxy = false, this.autoLaunch = false, this.silentLaunch = false, this.autoRun = false, this.openLogs = false, this.closeConnections = true, this.testUrl = defaultTestUrl, this.isAnimateToPage = true, this.autoCheckUpdate = true, this.showLabel = false, this.disclaimerAccepted = false, this.crashlyticsTip = false, this.crashlytics = false, this.minimizeOnExit = true, this.hidden = false, this.developerMode = false, this.recoveryStrategy = RecoveryStrategy.compatible}): _dashboardWidgets = dashboardWidgets;
factory _AppSettingProps.fromJson(Map<String, dynamic> json) => _$AppSettingPropsFromJson(json);
@override final String? locale;
@@ -247,6 +249,8 @@ class _AppSettingProps implements AppSettingProps {
@override@JsonKey() final bool autoCheckUpdate;
@override@JsonKey() final bool showLabel;
@override@JsonKey() final bool disclaimerAccepted;
@override@JsonKey() final bool crashlyticsTip;
@override@JsonKey() final bool crashlytics;
@override@JsonKey() final bool minimizeOnExit;
@override@JsonKey() final bool hidden;
@override@JsonKey() final bool developerMode;
@@ -265,16 +269,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other._dashboardWidgets, _dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.recoveryStrategy, recoveryStrategy) || other.recoveryStrategy == recoveryStrategy));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettingProps&&(identical(other.locale, locale) || other.locale == locale)&&const DeepCollectionEquality().equals(other._dashboardWidgets, _dashboardWidgets)&&(identical(other.onlyStatisticsProxy, onlyStatisticsProxy) || other.onlyStatisticsProxy == onlyStatisticsProxy)&&(identical(other.autoLaunch, autoLaunch) || other.autoLaunch == autoLaunch)&&(identical(other.silentLaunch, silentLaunch) || other.silentLaunch == silentLaunch)&&(identical(other.autoRun, autoRun) || other.autoRun == autoRun)&&(identical(other.openLogs, openLogs) || other.openLogs == openLogs)&&(identical(other.closeConnections, closeConnections) || other.closeConnections == closeConnections)&&(identical(other.testUrl, testUrl) || other.testUrl == testUrl)&&(identical(other.isAnimateToPage, isAnimateToPage) || other.isAnimateToPage == isAnimateToPage)&&(identical(other.autoCheckUpdate, autoCheckUpdate) || other.autoCheckUpdate == autoCheckUpdate)&&(identical(other.showLabel, showLabel) || other.showLabel == showLabel)&&(identical(other.disclaimerAccepted, disclaimerAccepted) || other.disclaimerAccepted == disclaimerAccepted)&&(identical(other.crashlyticsTip, crashlyticsTip) || other.crashlyticsTip == crashlyticsTip)&&(identical(other.crashlytics, crashlytics) || other.crashlytics == crashlytics)&&(identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.developerMode, developerMode) || other.developerMode == developerMode)&&(identical(other.recoveryStrategy, recoveryStrategy) || other.recoveryStrategy == recoveryStrategy));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,locale,const DeepCollectionEquality().hash(_dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,minimizeOnExit,hidden,developerMode,recoveryStrategy);
int get hashCode => Object.hashAll([runtimeType,locale,const DeepCollectionEquality().hash(_dashboardWidgets),onlyStatisticsProxy,autoLaunch,silentLaunch,autoRun,openLogs,closeConnections,testUrl,isAnimateToPage,autoCheckUpdate,showLabel,disclaimerAccepted,crashlyticsTip,crashlytics,minimizeOnExit,hidden,developerMode,recoveryStrategy]);
@override
String toString() {
return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, recoveryStrategy: $recoveryStrategy)';
return 'AppSettingProps(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyStatisticsProxy: $onlyStatisticsProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, crashlyticsTip: $crashlyticsTip, crashlytics: $crashlytics, minimizeOnExit: $minimizeOnExit, hidden: $hidden, developerMode: $developerMode, recoveryStrategy: $recoveryStrategy)';
}
@@ -285,7 +289,7 @@ abstract mixin class _$AppSettingPropsCopyWith<$Res> implements $AppSettingProps
factory _$AppSettingPropsCopyWith(_AppSettingProps value, $Res Function(_AppSettingProps) _then) = __$AppSettingPropsCopyWithImpl;
@override @useResult
$Res call({
String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy
String? locale,@JsonKey(fromJson: dashboardWidgetsSafeFormJson) List<DashboardWidget> dashboardWidgets, bool onlyStatisticsProxy, bool autoLaunch, bool silentLaunch, bool autoRun, bool openLogs, bool closeConnections, String testUrl, bool isAnimateToPage, bool autoCheckUpdate, bool showLabel, bool disclaimerAccepted, bool crashlyticsTip, bool crashlytics, bool minimizeOnExit, bool hidden, bool developerMode, RecoveryStrategy recoveryStrategy
});
@@ -302,7 +306,7 @@ class __$AppSettingPropsCopyWithImpl<$Res>
/// Create a copy of AppSettingProps
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? recoveryStrategy = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? locale = freezed,Object? dashboardWidgets = null,Object? onlyStatisticsProxy = null,Object? autoLaunch = null,Object? silentLaunch = null,Object? autoRun = null,Object? openLogs = null,Object? closeConnections = null,Object? testUrl = null,Object? isAnimateToPage = null,Object? autoCheckUpdate = null,Object? showLabel = null,Object? disclaimerAccepted = null,Object? crashlyticsTip = null,Object? crashlytics = null,Object? minimizeOnExit = null,Object? hidden = null,Object? developerMode = null,Object? recoveryStrategy = null,}) {
return _then(_AppSettingProps(
locale: freezed == locale ? _self.locale : locale // ignore: cast_nullable_to_non_nullable
as String?,dashboardWidgets: null == dashboardWidgets ? _self._dashboardWidgets : dashboardWidgets // ignore: cast_nullable_to_non_nullable
@@ -317,6 +321,8 @@ as String,isAnimateToPage: null == isAnimateToPage ? _self.isAnimateToPage : isA
as bool,autoCheckUpdate: null == autoCheckUpdate ? _self.autoCheckUpdate : autoCheckUpdate // ignore: cast_nullable_to_non_nullable
as bool,showLabel: null == showLabel ? _self.showLabel : showLabel // ignore: cast_nullable_to_non_nullable
as bool,disclaimerAccepted: null == disclaimerAccepted ? _self.disclaimerAccepted : disclaimerAccepted // ignore: cast_nullable_to_non_nullable
as bool,crashlyticsTip: null == crashlyticsTip ? _self.crashlyticsTip : crashlyticsTip // ignore: cast_nullable_to_non_nullable
as bool,crashlytics: null == crashlytics ? _self.crashlytics : crashlytics // ignore: cast_nullable_to_non_nullable
as bool,minimizeOnExit: null == minimizeOnExit ? _self.minimizeOnExit : minimizeOnExit // ignore: cast_nullable_to_non_nullable
as bool,hidden: null == hidden ? _self.hidden : hidden // ignore: cast_nullable_to_non_nullable
as bool,developerMode: null == developerMode ? _self.developerMode : developerMode // ignore: cast_nullable_to_non_nullable

View File

@@ -23,6 +23,8 @@ _AppSettingProps _$AppSettingPropsFromJson(Map<String, dynamic> json) =>
autoCheckUpdate: json['autoCheckUpdate'] as bool? ?? true,
showLabel: json['showLabel'] as bool? ?? false,
disclaimerAccepted: json['disclaimerAccepted'] as bool? ?? false,
crashlyticsTip: json['crashlyticsTip'] as bool? ?? false,
crashlytics: json['crashlytics'] as bool? ?? false,
minimizeOnExit: json['minimizeOnExit'] as bool? ?? true,
hidden: json['hidden'] as bool? ?? false,
developerMode: json['developerMode'] as bool? ?? false,
@@ -51,6 +53,8 @@ Map<String, dynamic> _$AppSettingPropsToJson(_AppSettingProps instance) =>
'autoCheckUpdate': instance.autoCheckUpdate,
'showLabel': instance.showLabel,
'disclaimerAccepted': instance.disclaimerAccepted,
'crashlyticsTip': instance.crashlyticsTip,
'crashlytics': instance.crashlytics,
'minimizeOnExit': instance.minimizeOnExit,
'hidden': instance.hidden,
'developerMode': instance.developerMode,

View File

@@ -169,8 +169,8 @@ class ScannerOverlay extends CustomPainter {
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()
..addRRect(
RRect.fromRectAndCorners(
..addRSuperellipse(
RSuperellipse.fromRectAndCorners(
scanWindow,
topLeft: Radius.circular(borderRadius),
topRight: Radius.circular(borderRadius),
@@ -195,7 +195,7 @@ class ScannerOverlay extends CustomPainter {
..style = PaintingStyle.stroke
..strokeWidth = 4.0;
final borderRect = RRect.fromRectAndCorners(
final border = RSuperellipse.fromRectAndCorners(
scanWindow,
topLeft: Radius.circular(borderRadius),
topRight: Radius.circular(borderRadius),
@@ -204,7 +204,7 @@ class ScannerOverlay extends CustomPainter {
);
canvas.drawPath(backgroundWithCutout, backgroundPaint);
canvas.drawRRect(borderRect, borderPaint);
canvas.drawRSuperellipse(border, borderPaint);
}
@override

View File

@@ -86,7 +86,11 @@ class Service {
}
Future<String> init() async {
return await methodChannel.invokeMethod<String>('init') ?? '';
return await methodChannel.invokeMethod<String>(
'init',
!globalState.isService,
) ??
'';
}
Future<bool> shutdown() async {

View File

@@ -6,6 +6,9 @@ part of '../app.dart';
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(RealTunEnable)
const realTunEnableProvider = RealTunEnableProvider._();
@@ -1486,6 +1489,3 @@ abstract class _$QueryMap extends $Notifier<Map<QueryTag, String>> {
element.handleValue(ref, created);
}
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -6,6 +6,9 @@ part of '../config.dart';
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AppSetting)
const appSettingProvider = AppSettingProvider._();
@@ -693,6 +696,3 @@ abstract class _$PatchClashConfig extends $Notifier<ClashConfig> {
element.handleValue(ref, created);
}
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -6,6 +6,9 @@ part of '../state.dart';
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(configState)
const configStateProvider = ConfigStateProvider._();
@@ -431,7 +434,7 @@ final class ContentWidthProvider
}
}
String _$contentWidthHash() => r'9ac0dc2bcdc957160f59d6065cd7f846f41a95b1';
String _$contentWidthHash() => r'4710fee8f91b08dc88520798fb6b1d4f61f090e6';
@ProviderFor(dashboardState)
const dashboardStateProvider = DashboardStateProvider._();
@@ -1952,8 +1955,14 @@ final class GetProfileOverrideDataFamily extends $Family
@ProviderFor(layoutChange)
const layoutChangeProvider = LayoutChangeProvider._();
final class LayoutChangeProvider extends $FunctionalProvider<VM2?, VM2?, VM2?>
with $Provider<VM2?> {
final class LayoutChangeProvider
extends
$FunctionalProvider<
VM2<dynamic, dynamic>?,
VM2<dynamic, dynamic>?,
VM2<dynamic, dynamic>?
>
with $Provider<VM2<dynamic, dynamic>?> {
const LayoutChangeProvider._()
: super(
from: null,
@@ -1970,19 +1979,20 @@ final class LayoutChangeProvider extends $FunctionalProvider<VM2?, VM2?, VM2?>
@$internal
@override
$ProviderElement<VM2?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
$ProviderElement<VM2<dynamic, dynamic>?> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
VM2? create(Ref ref) {
VM2<dynamic, dynamic>? create(Ref ref) {
return layoutChange(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(VM2? value) {
Override overrideWithValue(VM2<dynamic, dynamic>? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<VM2?>(value),
providerOverride: $SyncValueProvider<VM2<dynamic, dynamic>?>(value),
);
}
}
@@ -2342,7 +2352,7 @@ final class AndroidStateProvider
}
}
String _$androidStateHash() => r'bdf7baeb4ed4041233f06f0aede8d4d41489ebd6';
String _$androidStateHash() => r'9f527fbb00c7e0c177f023e77d2f23458543d72f';
@ProviderFor(Query)
const queryProvider = QueryFamily._();
@@ -2432,6 +2442,3 @@ abstract class _$Query extends $Notifier<String> {
element.handleValue(ref, created);
}
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -629,10 +629,14 @@ AndroidState androidState(Ref ref) {
appSettingProvider.select((state) => state.onlyStatisticsProxy),
);
ref.watch((appSettingProvider).select((state) => state.locale));
final crashlytics = ref.watch(
(appSettingProvider).select((state) => state.crashlytics),
);
return AndroidState(
currentProfileName: currentProfileName,
onlyStatisticsProxy: onlyStatisticsProxy,
stopText: appLocalizations.stop,
crashlytics: crashlytics,
);
}

View File

@@ -48,6 +48,7 @@ class GlobalState {
AppController? _appController;
bool isInit = false;
bool isUserDisconnected = false;
bool isService = false;
bool get isStart => startTime != null && startTime!.isBeforeNow;
@@ -188,9 +189,11 @@ class GlobalState {
String? title,
String? confirmText,
bool cancelable = true,
bool? dismissible,
}) async {
return await showCommonDialog<bool>(
context: context,
dismissible: dismissible,
child: Builder(
builder: (context) {
return CommonDialog(
@@ -249,14 +252,14 @@ class GlobalState {
Future<T?> showCommonDialog<T>({
required Widget child,
BuildContext? context,
bool dismissible = true,
bool? dismissible,
}) async {
return await showModal<T>(
useRootNavigator: false,
context: context ?? globalState.navigatorKey.currentContext!,
configuration: FadeScaleTransitionConfiguration(
barrierColor: Colors.black38,
barrierDismissible: dismissible,
barrierDismissible: dismissible ?? true,
),
builder: (_) => child,
filter: commonFilter,
@@ -325,6 +328,7 @@ class GlobalState {
currentProfileName: config.currentProfile?.label ?? '',
onlyStatisticsProxy: config.appSetting.onlyStatisticsProxy,
stopText: appLocalizations.stop,
crashlytics: config.appSetting.crashlytics,
);
}

View File

@@ -214,6 +214,29 @@ class OpenLogsItem extends ConsumerWidget {
}
}
class CrashlyticsItem extends ConsumerWidget {
const CrashlyticsItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final crashlytics = ref.watch(
appSettingProvider.select((state) => state.crashlytics),
);
return ListItem.switchItem(
title: Text(appLocalizations.crashlytics),
subtitle: Text(appLocalizations.crashlyticsTip),
delegate: SwitchDelegate(
value: crashlytics,
onChanged: (bool value) {
ref
.read(appSettingProvider.notifier)
.updateState((state) => state.copyWith(crashlytics: value));
},
),
);
}
}
class AutoCheckUpdateItem extends ConsumerWidget {
const AutoCheckUpdateItem({super.key});
@@ -256,6 +279,7 @@ class ApplicationSettingView extends StatelessWidget {
OpenLogsItem(),
CloseConnectionsItem(),
UsageItem(),
if (system.isAndroid) CrashlyticsItem(),
AutoCheckUpdateItem(),
];
return ListView.separated(

View File

@@ -51,7 +51,7 @@ class UaItem extends ConsumerWidget {
subtitle: Text(globalUa ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>(
title: 'UA',
options: [null, 'clash-verge/v1.6.6', 'ClashforWindows/0.19.23'],
options: [null, 'clash-verge/v2.4.2', 'ClashforWindows/0.19.23'],
value: globalUa,
onChanged: (value) {
ref

View File

@@ -249,6 +249,7 @@ class TrackerInfoDetailView extends StatelessWidget {
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
Text(appLocalizations.proxyChains),
Flexible(child: chains),

View File

@@ -14,7 +14,7 @@ class StartButton extends ConsumerStatefulWidget {
class _StartButtonState extends ConsumerState<StartButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
AnimationController? _controller;
late Animation<double> _animation;
bool isStart = false;
@@ -28,7 +28,7 @@ class _StartButtonState extends ConsumerState<StartButton>
duration: const Duration(milliseconds: 200),
);
_animation = CurvedAnimation(
parent: _controller,
parent: _controller!,
curve: Curves.easeOutBack,
);
ref.listenManual(runTimeProvider.select((state) => state != null), (
@@ -44,7 +44,8 @@ class _StartButtonState extends ConsumerState<StartButton>
@override
void dispose() {
_controller.dispose();
_controller?.dispose();
_controller = null;
super.dispose();
}
@@ -59,9 +60,9 @@ class _StartButtonState extends ConsumerState<StartButton>
void updateController() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isStart && mounted) {
_controller.forward();
_controller?.forward();
} else {
_controller.reverse();
_controller?.reverse();
}
});
}
@@ -74,12 +75,13 @@ class _StartButtonState extends ConsumerState<StartButton>
}
return Theme(
data: Theme.of(context).copyWith(
floatingActionButtonTheme: FloatingActionButtonThemeData(
sizeConstraints: BoxConstraints(minWidth: 56, maxWidth: 200),
),
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
.copyWith(
sizeConstraints: BoxConstraints(minWidth: 56, maxWidth: 200),
),
),
child: AnimatedBuilder(
animation: _controller.view,
animation: _controller!.view,
builder: (_, child) {
final textWidth =
globalState.measure

View File

@@ -128,10 +128,11 @@ class TrafficUsage extends StatelessWidget {
Container(
width: 20,
height: 8,
decoration: BoxDecoration(
decoration: ShapeDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(
3,
shape: RoundedSuperellipseBorder(
borderRadius:
BorderRadius.circular(3),
),
),
),
@@ -151,10 +152,11 @@ class TrafficUsage extends StatelessWidget {
Container(
width: 20,
height: 8,
decoration: BoxDecoration(
decoration: ShapeDecoration(
color: secondaryColor,
borderRadius: BorderRadius.circular(
3,
shape: RoundedSuperellipseBorder(
borderRadius:
BorderRadius.circular(3),
),
),
),

View File

@@ -2,10 +2,10 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/core/controller.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -18,12 +18,14 @@ class DeveloperView extends ConsumerWidget {
items: [
ListItem(
title: Text(appLocalizations.messageTest),
minVerticalPadding: 14,
onTap: () {
context.showNotifier(appLocalizations.messageTestTip);
},
),
ListItem(
title: Text(appLocalizations.logsTest),
minVerticalPadding: 14,
onTap: () {
for (int i = 0; i < 1000; i++) {
globalState.appController.addLog(
@@ -36,24 +38,29 @@ class DeveloperView extends ConsumerWidget {
),
ListItem(
title: Text(appLocalizations.crashTest),
minVerticalPadding: 14,
onTap: () {
coreController.crash();
if (kDebugMode) {
coreController.crash();
}
},
),
ListItem(
title: Text(appLocalizations.clearData),
minVerticalPadding: 14,
onTap: () async {
await globalState.appController.handleClear();
},
),
ListItem(
title: Text('Loading'),
onTap: () {
ref.read(loadingProvider.notifier).value = !ref.read(
loadingProvider,
);
},
),
// ListItem(
// title: Text('Loading'),
// minVerticalPadding: 14,
// onTap: () {
// ref.read(loadingProvider.notifier).value = !ref.read(
// loadingProvider,
// );
// },
// ),
],
);
}

View File

@@ -360,7 +360,7 @@ class DelayTestButton extends StatefulWidget {
class _DelayTestButtonState extends State<DelayTestButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
late Animation<double> _animation;
Future<void> _healthcheck() async {
if (_controller.isAnimating) {
@@ -378,10 +378,10 @@ class _DelayTestButtonState extends State<DelayTestButton>
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
duration: const Duration(milliseconds: 400),
);
_scale = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller, curve: const Interval(0, 1)),
_animation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack),
);
}
@@ -399,7 +399,10 @@ class _DelayTestButtonState extends State<DelayTestButton>
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(scale: _scale.value, child: child),
child: FadeTransition(
opacity: _animation,
child: ScaleTransition(scale: _animation, child: child),
),
);
},
child: FloatingActionButton(

View File

@@ -29,10 +29,10 @@ class ResourcesView extends StatelessWidget {
@override
Widget build(BuildContext context) {
const geoItems = <GeoItem>[
GeoItem(label: 'GeoIp', fileName: geoIpFileName, key: 'geoip'),
GeoItem(label: 'GeoSite', fileName: geoSiteFileName, key: 'geosite'),
GeoItem(label: 'MMDB', fileName: mmdbFileName, key: 'mmdb'),
GeoItem(label: 'ASN', fileName: asnFileName, key: 'asn'),
GeoItem(label: 'GEOIP', fileName: GEOIP, key: 'geoip'),
GeoItem(label: 'GEOSITE', fileName: GEOSITE, key: 'geosite'),
GeoItem(label: 'MMDB', fileName: MMDB, key: 'mmdb'),
GeoItem(label: 'ASN', fileName: ASN, key: 'asn'),
];
return CommonScaffold(

View File

@@ -177,7 +177,9 @@ class CommonCard extends StatelessWidget {
style: ButtonStyle(
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius)),
RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(radius),
),
),
iconColor: WidgetStatePropertyAll(context.colorScheme.primary),
iconSize: WidgetStateProperty.all(20),

View File

@@ -37,7 +37,7 @@ class ColorSchemeBox extends StatelessWidget {
),
child: Container(
padding: const EdgeInsets.all(8),
child: ClipRRect(
child: ClipRSuperellipse(
borderRadius: BorderRadius.circular(36),
child: SizedBox(
width: 72,
@@ -47,22 +47,16 @@ class ColorSchemeBox extends StatelessWidget {
children: [
GridItem(
mainAxisCellCount: 2,
child: Container(
color: colorScheme.primary,
),
child: Container(color: colorScheme.primary),
),
GridItem(
mainAxisCellCount: 1,
child: Container(
color: colorScheme.secondary,
),
child: Container(color: colorScheme.secondary),
),
GridItem(
mainAxisCellCount: 1,
child: Container(
color: colorScheme.tertiary,
),
)
child: Container(color: colorScheme.tertiary),
),
],
),
),
@@ -73,11 +67,8 @@ class ColorSchemeBox extends StatelessWidget {
const Positioned(
bottom: 4,
right: 4,
child: Icon(
Icons.colorize,
size: 20,
),
)
child: Icon(Icons.colorize, size: 20),
),
],
);
},
@@ -112,9 +103,7 @@ class PrimaryColorBox extends ConsumerWidget {
),
);
return Theme(
data: themeData.copyWith(
colorScheme: colorScheme,
),
data: themeData.copyWith(colorScheme: colorScheme),
child: child,
);
}

View File

@@ -18,30 +18,21 @@ class RadioDelegate<T> extends Delegate {
final T value;
final void Function()? onTab;
const RadioDelegate({
required this.value,
this.onTab,
});
const RadioDelegate({required this.value, this.onTab});
}
class SwitchDelegate<T> extends Delegate {
final bool value;
final ValueChanged<bool>? onChanged;
const SwitchDelegate({
required this.value,
this.onChanged,
});
const SwitchDelegate({required this.value, this.onChanged});
}
class CheckboxDelegate<T> extends Delegate {
final bool value;
final ValueChanged<bool?>? onChanged;
const CheckboxDelegate({
this.value = false,
this.onChanged,
});
const CheckboxDelegate({this.value = false, this.onChanged});
}
class OpenDelegate extends Delegate {
@@ -129,6 +120,7 @@ class ListItem<T> extends StatelessWidget {
final double? horizontalTitleGap;
final TextStyle? titleTextStyle;
final TextStyle? subtitleTextStyle;
final double minVerticalPadding;
final void Function()? onTap;
const ListItem({
@@ -143,6 +135,7 @@ class ListItem<T> extends StatelessWidget {
this.onTap,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : delegate = const Delegate();
@@ -158,6 +151,7 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTap = null;
@@ -173,6 +167,7 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTap = null;
@@ -188,6 +183,7 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTap = null;
@@ -203,6 +199,7 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTap = null;
@@ -217,9 +214,10 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTap = null;
}) : trailing = null,
onTap = null;
const ListItem.switchItem({
super.key,
@@ -232,9 +230,10 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTap = null;
}) : trailing = null,
onTap = null;
const ListItem.radio({
super.key,
@@ -247,9 +246,10 @@ class ListItem<T> extends StatelessWidget {
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.minVerticalPadding = 12,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : leading = null,
onTap = null;
}) : leading = null,
onTap = null;
Widget _buildListTile({
void Function()? onTap,
@@ -264,7 +264,7 @@ class ListItem<T> extends StatelessWidget {
leading: leading ?? this.leading,
horizontalTitleGap: horizontalTitleGap,
title: title,
minVerticalPadding: 12,
minVerticalPadding: minVerticalPadding,
subtitle: subtitle,
titleAlignment: tileTitleAlignment,
onTap: onTap,
@@ -282,7 +282,7 @@ class ListItem<T> extends StatelessWidget {
closedBuilder: (_, action) {
openAction() {
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
if (!isMobile || system.isDesktop) {
if (!isMobile) {
showExtend(
context,
props: ExtendProps(
@@ -306,9 +306,7 @@ class ListItem<T> extends StatelessWidget {
action();
}
return _buildListTile(
onTap: openAction,
);
return _buildListTile(onTap: openAction);
},
openBuilder: (_, action) {
return openDelegate.wrap
@@ -422,9 +420,7 @@ class ListItem<T> extends StatelessWidget {
);
}
return _buildListTile(
onTap: onTap,
);
return _buildListTile(onTap: onTap);
}
}
@@ -448,13 +444,9 @@ class ListHeader extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: padding ??
const EdgeInsets.only(
left: 16,
right: 8,
top: 24,
bottom: 8,
),
padding:
padding ??
const EdgeInsets.only(left: 16, right: 8, top: 24, bottom: 8),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -466,19 +458,18 @@ class ListHeader extends StatelessWidget {
Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.opacity80,
fontWeight: FontWeight.w600,
),
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.opacity80,
fontWeight: FontWeight.w600,
),
),
if (subTitle != null)
Text(
subTitle!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
color: Theme.of(context).colorScheme.outline,
),
),
],
),
@@ -486,12 +477,7 @@ class ListHeader extends StatelessWidget {
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
...genActions(
actions,
space: space,
),
],
children: [...genActions(actions, space: space)],
),
],
),
@@ -506,18 +492,11 @@ List<Widget> generateSection({
bool separated = true,
}) {
final genItems = separated
? items.separated(
const Divider(
height: 0,
),
)
? items.separated(const Divider(height: 0))
: items;
return [
if (items.isNotEmpty && title != null)
ListHeader(
title: title,
actions: actions,
),
ListHeader(title: title, actions: actions),
...genItems,
];
}
@@ -528,22 +507,26 @@ Widget generateSectionV2({
List<Widget>? actions,
bool separated = true,
}) {
final genItems = items
.map<Widget>((item) {
return ClipRSuperellipse(
borderRadius: BorderRadius.circular(4),
child: CommonCard(
type: CommonCardType.filled,
radius: 0,
child: item,
),
);
})
.separated(const Divider(height: 2, color: Colors.transparent));
return Column(
children: [
if (items.isNotEmpty && title != null)
ListHeader(
title: title,
actions: actions,
),
CommonCard(
radius: 18,
type: CommonCardType.filled,
child: Column(
children: [
...items,
],
),
)
ListHeader(title: title, actions: actions),
ClipRSuperellipse(
borderRadius: BorderRadius.circular(14),
child: Column(children: [...genItems]),
),
],
);
}
@@ -555,18 +538,10 @@ List<Widget> generateInfoSection({
bool separated = true,
}) {
final genItems = separated
? items.separated(
const Divider(
height: 0,
),
)
? items.separated(const Divider(height: 0))
: items;
return [
if (items.isNotEmpty)
InfoHeader(
info: info,
actions: actions,
),
if (items.isNotEmpty) InfoHeader(info: info, actions: actions),
...genItems,
];
}
@@ -575,8 +550,6 @@ Widget generateListView(List<Widget> items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) => items[index],
padding: const EdgeInsets.only(
bottom: 16,
),
padding: const EdgeInsets.only(bottom: 16),
);
}

View File

@@ -2,19 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
typedef CloseContainerActionCallback<S> = void Function({S? returnValue});
typedef OpenContainerBuilder<S> = Widget Function(
BuildContext context,
CloseContainerActionCallback<S> action,
);
typedef CloseContainerBuilder = Widget Function(
BuildContext context,
VoidCallback action,
);
typedef OpenContainerBuilder<S> =
Widget Function(
BuildContext context,
CloseContainerActionCallback<S> action,
);
typedef CloseContainerBuilder =
Widget Function(BuildContext context, VoidCallback action);
enum ContainerTransitionType {
fade,
fadeThrough,
}
enum ContainerTransitionType { fade, fadeThrough }
typedef ClosedCallback<S> = void Function(S data);
@@ -56,20 +52,23 @@ class _OpenContainerState<T> extends State<OpenContainer<T?>> {
Future<void> openContainer() async {
final Color middleColor =
widget.middleColor ?? Theme.of(context).canvasColor;
final T? data = await Navigator.of(
context,
rootNavigator: widget.useRootNavigator,
).push(_OpenContainerRoute<T>(
middleColor: middleColor,
closedBuilder: widget.closedBuilder,
openBuilder: widget.openBuilder,
hideableKey: _hideableKey,
closedBuilderKey: _closedBuilderKey,
transitionDuration: widget.transitionDuration,
transitionType: widget.transitionType,
useRootNavigator: widget.useRootNavigator,
routeSettings: widget.routeSettings,
));
final T? data =
await Navigator.of(
context,
rootNavigator: widget.useRootNavigator,
).push(
_OpenContainerRoute<T>(
middleColor: middleColor,
closedBuilder: widget.closedBuilder,
openBuilder: widget.openBuilder,
hideableKey: _hideableKey,
closedBuilderKey: _closedBuilderKey,
transitionDuration: widget.transitionDuration,
transitionType: widget.transitionType,
useRootNavigator: widget.useRootNavigator,
routeSettings: widget.routeSettings,
),
);
if (widget.onClosed != null) {
widget.onClosed!(data);
}
@@ -97,10 +96,7 @@ class _OpenContainerState<T> extends State<OpenContainer<T?>> {
}
class _Hideable extends StatefulWidget {
const _Hideable({
super.key,
required this.child,
});
const _Hideable({super.key, required this.child});
final Widget child;
@@ -161,9 +157,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
required this.transitionType,
required this.useRootNavigator,
required RouteSettings? routeSettings,
}) : _closedOpacityTween = _getClosedOpacityTween(transitionType),
_openOpacityTween = _getOpenOpacityTween(transitionType),
super(settings: routeSettings);
}) : _closedOpacityTween = _getClosedOpacityTween(transitionType),
_openOpacityTween = _getOpenOpacityTween(transitionType),
super(settings: routeSettings);
static _FlippableTweenSequence<Color?> _getColorTween({
required ContainerTransitionType transitionType,
@@ -173,99 +169,89 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
}) {
switch (transitionType) {
case ContainerTransitionType.fade:
return _FlippableTweenSequence<Color?>(
<TweenSequenceItem<Color?>>[
TweenSequenceItem<Color>(
tween: ConstantTween<Color>(closedColor),
weight: 1 / 5,
),
TweenSequenceItem<Color?>(
tween: ColorTween(begin: closedColor, end: openColor),
weight: 1 / 5,
),
TweenSequenceItem<Color>(
tween: ConstantTween<Color>(openColor),
weight: 3 / 5,
),
],
);
return _FlippableTweenSequence<Color?>(<TweenSequenceItem<Color?>>[
TweenSequenceItem<Color>(
tween: ConstantTween<Color>(closedColor),
weight: 1 / 5,
),
TweenSequenceItem<Color?>(
tween: ColorTween(begin: closedColor, end: openColor),
weight: 1 / 5,
),
TweenSequenceItem<Color>(
tween: ConstantTween<Color>(openColor),
weight: 3 / 5,
),
]);
case ContainerTransitionType.fadeThrough:
return _FlippableTweenSequence<Color?>(
<TweenSequenceItem<Color?>>[
TweenSequenceItem<Color?>(
tween: ColorTween(begin: closedColor, end: middleColor),
weight: 1 / 5,
),
TweenSequenceItem<Color?>(
tween: ColorTween(begin: middleColor, end: openColor),
weight: 4 / 5,
),
],
);
return _FlippableTweenSequence<Color?>(<TweenSequenceItem<Color?>>[
TweenSequenceItem<Color?>(
tween: ColorTween(begin: closedColor, end: middleColor),
weight: 1 / 5,
),
TweenSequenceItem<Color?>(
tween: ColorTween(begin: middleColor, end: openColor),
weight: 4 / 5,
),
]);
}
}
static _FlippableTweenSequence<double> _getClosedOpacityTween(
ContainerTransitionType transitionType) {
ContainerTransitionType transitionType,
) {
switch (transitionType) {
case ContainerTransitionType.fade:
return _FlippableTweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(1.0),
weight: 1,
),
],
);
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(1.0),
weight: 1,
),
]);
case ContainerTransitionType.fadeThrough:
return _FlippableTweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1.0, end: 0.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 4 / 5,
),
],
);
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1.0, end: 0.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 4 / 5,
),
]);
}
}
static _FlippableTweenSequence<double> _getOpenOpacityTween(
ContainerTransitionType transitionType) {
ContainerTransitionType transitionType,
) {
switch (transitionType) {
case ContainerTransitionType.fade:
return _FlippableTweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(1.0),
weight: 3 / 5,
),
],
);
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(1.0),
weight: 3 / 5,
),
]);
case ContainerTransitionType.fadeThrough:
return _FlippableTweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
weight: 4 / 5,
),
],
);
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0.0),
weight: 1 / 5,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
weight: 4 / 5,
),
]);
}
}
@@ -325,8 +311,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
@override
void dispose() {
if (hideableKey.currentState?.isVisible == false) {
SchedulerBinding.instance
.addPostFrameCallback((Duration d) => _toggleHideable(hide: false));
SchedulerBinding.instance.addPostFrameCallback(
(Duration d) => _toggleHideable(hide: false),
);
}
super.dispose();
}
@@ -343,10 +330,12 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
required BuildContext navigatorContext,
bool delayForSourceRoute = false,
}) {
final RenderBox navigator = Navigator.of(
navigatorContext,
rootNavigator: useRootNavigator,
).context.findRenderObject()! as RenderBox;
final RenderBox navigator =
Navigator.of(
navigatorContext,
rootNavigator: useRootNavigator,
).context.findRenderObject()!
as RenderBox;
final Size navSize = _getSize(navigator);
_rectTween.end = Offset.zero & navSize;
@@ -359,8 +348,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
}
if (delayForSourceRoute) {
SchedulerBinding.instance
.addPostFrameCallback(takeMeasurementsInSourceRoute);
SchedulerBinding.instance.addPostFrameCallback(
takeMeasurementsInSourceRoute,
);
} else {
takeMeasurementsInSourceRoute();
}
@@ -451,8 +441,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
final Animation<double> curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.fastOutSlowIn,
reverseCurve:
_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
reverseCurve: _transitionWasInterrupted
? null
: Curves.fastOutSlowIn.flipped,
);
TweenSequence<Color?>? colorTween;
TweenSequence<double>? closedOpacityTween, openOpacityTween;
@@ -508,8 +499,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
child: (hideableKey.currentState?.isInTree ?? false)
? null
: FadeTransition(
opacity:
closedOpacityTween!.animate(animation),
opacity: closedOpacityTween!.animate(
animation,
),
child: Builder(
key: closedBuilderKey,
builder: (BuildContext context) {
@@ -521,22 +513,18 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
),
),
),
// Open child fading in.
FittedBox(
fit: BoxFit.fitWidth,
OverflowBox(
maxWidth: _rectTween.end!.width,
maxHeight: _rectTween.end!.height,
alignment: Alignment.topLeft,
child: SizedBox(
width: _rectTween.end!.width,
height: _rectTween.end!.height,
child: FadeTransition(
opacity: openOpacityTween!.animate(animation),
child: Builder(
key: _openBuilderKey,
builder: (BuildContext context) {
return openBuilder(context, closeContainer);
},
),
child: FadeTransition(
opacity: openOpacityTween!.animate(animation),
child: Builder(
key: _openBuilderKey,
builder: (BuildContext context) {
return openBuilder(context, closeContainer);
},
),
),
),
@@ -578,10 +566,12 @@ class _FlippableTweenSequence<T> extends TweenSequence<T> {
if (_flipped == null) {
final List<TweenSequenceItem<T>> newItems = <TweenSequenceItem<T>>[];
for (int i = 0; i < _items.length; i++) {
newItems.add(TweenSequenceItem<T>(
tween: _items[i].tween,
weight: _items[_items.length - 1 - i].weight,
));
newItems.add(
TweenSequenceItem<T>(
tween: _items[i].tween,
weight: _items[_items.length - 1 - i].weight,
),
);
}
_flipped = _FlippableTweenSequence<T>(newItems);
}

View File

@@ -243,7 +243,7 @@ class _ShadePainter extends CustomPainter {
effectiveSquareRadius * 2,
effectiveSquareRadius * 2,
);
final RRect rRect = RRect.fromRectAndRadius(
final RSuperellipse rSuperellipse = RSuperellipse.fromRectAndRadius(
rectBox,
Radius.circular(trackBorderRadius),
);
@@ -254,8 +254,8 @@ class _ShadePainter extends CustomPainter {
HSVColor.fromAHSV(1, colorHue, 1, 1).toColor(),
],
).createShader(rectBox);
canvas.drawRRect(
rRect,
canvas.drawRSuperellipse(
rSuperellipse,
Paint()
..style = PaintingStyle.fill
..shader = horizontal,
@@ -266,8 +266,8 @@ class _ShadePainter extends CustomPainter {
end: Alignment.bottomCenter,
colors: <Color>[Colors.transparent, Colors.black],
).createShader(rectBox);
canvas.drawRRect(
rRect,
canvas.drawRSuperellipse(
rSuperellipse,
Paint()
..style = PaintingStyle.fill
..shader = vertical,

View File

@@ -38,10 +38,9 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
Widget child,
) {
final align = Alignment.topRight;
final animationValue = CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
).value;
final curveAnimation = animation
.drive(Tween(begin: 0.0, end: 1.0))
.drive(CurveTween(curve: Curves.easeOutBack));
return SafeArea(
child: ValueListenableBuilder(
valueListenable: offsetNotifier,
@@ -58,15 +57,17 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
},
child: AnimatedBuilder(
animation: animation,
builder: (_, Widget? child) {
return Opacity(
opacity: 0.1 + 0.9 * animationValue,
child: Transform.scale(
builder: (_, child) {
return FadeTransition(
opacity: curveAnimation,
child: ScaleTransition(
alignment: align,
scale: 0.7 + 0.3 * animationValue,
child: Transform.translate(
offset: Offset(0, -10) * (1 - animationValue),
child: child!,
scale: curveAnimation,
child: SlideTransition(
position: curveAnimation.drive(
Tween(begin: const Offset(0, -0.02), end: Offset.zero),
),
child: child,
),
),
);
@@ -78,7 +79,7 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
}
@override
Duration get transitionDuration => const Duration(milliseconds: 150);
Duration get transitionDuration => const Duration(milliseconds: 250);
}
class PopupController extends ValueNotifier<bool> {
@@ -270,8 +271,8 @@ class CommonPopupMenu extends StatelessWidget {
elevation: 12,
color: context.colorScheme.surfaceContainer,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(14),
),
child: Column(
mainAxisSize: MainAxisSize.min,

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