diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e7fda7f..f4d078f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,7 +19,7 @@ jobs: os: windows-latest arch: amd64 - platform: linux - os: ubuntu-latest + os: ubuntu-22.04 arch: amd64 - platform: macos os: macos-13 @@ -201,6 +201,7 @@ jobs: env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TAG: ${{ github.ref_name }} + RUN_ID: ${{ github.run_id }} run: | python -m pip install --upgrade pip pip install requests @@ -211,6 +212,14 @@ jobs: version=$(echo "${{ github.ref_name }}" | sed 's/^v//') sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md + - name: Generate sha256 + if: env.IS_STABLE == 'true' + run: | + cd ./dist + for file in $(find . -type f -not -name "*.sha256"); do + sha256sum "$file" > "${file}.sha256" + done + - name: Release if: ${{ env.IS_STABLE == 'true' }} uses: softprops/action-gh-release@v2 diff --git a/README.md b/README.md index b45f03c..4255f2c 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ on Mobile: ⚠️ Make sure to install the following dependencies before using them ```bash - sudo apt-get install appindicator3-0.1 libappindicator3-dev - sudo apt-get install keybinder-3.0 + sudo apt-get install libayatana-appindicator3-dev + sudo apt-get install libkeybinder-3.0-dev ``` ### Android diff --git a/README_zh_CN.md b/README_zh_CN.md index 59ccb34..54d60af 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -41,8 +41,8 @@ on Mobile: ⚠️ 使用前请确保安装以下依赖 ```bash - sudo apt-get install appindicator3-0.1 libappindicator3-dev - sudo apt-get install keybinder-3.0 + sudo apt-get install libayatana-appindicator3-dev + sudo apt-get install libkeybinder-3.0-dev ``` ### Android diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 606a14a..ece0d77 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ + diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt index d2b8d4c..052b958 100644 --- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -20,6 +20,10 @@ enum class RunState { object GlobalState { val runLock = ReentrantLock() + const val NOTIFICATION_CHANNEL = "FlClash" + + const val NOTIFICATION_ID = 1 + val runState: MutableLiveData = MutableLiveData(RunState.STOP) var flutterEngine: FlutterEngine? = null private var serviceEngine: FlutterEngine? = null diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt index 6e186da..c404697 100644 --- a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt @@ -1,6 +1,5 @@ package com.follow.clash -import com.follow.clash.core.Core import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.TilePlugin diff --git a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt index 2f1eb91..0aa09de 100644 --- a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt +++ b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt @@ -27,7 +27,6 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine - suspend fun Drawable.getBase64(): String { val drawable = this return withContext(Dispatchers.IO) { diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Package.kt b/android/app/src/main/kotlin/com/follow/clash/models/Package.kt index 967b5f3..0a6d6c7 100644 --- a/android/app/src/main/kotlin/com/follow/clash/models/Package.kt +++ b/android/app/src/main/kotlin/com/follow/clash/models/Package.kt @@ -3,6 +3,7 @@ package com.follow.clash.models data class Package( val packageName: String, val label: String, - val isSystem: Boolean, + val system: Boolean, + val internet: Boolean, val lastUpdateTime: Long, ) diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt index 12bdb39..757c193 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt @@ -293,19 +293,17 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware if (packages.isNotEmpty()) return packages packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS) ?.filter { - it.packageName != FlClashApplication.getAppContext().packageName && ( - it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true - || it.packageName == "android" - ) + it.packageName != FlClashApplication.getAppContext().packageName || it.packageName == "android" }?.map { - Package( - packageName = it.packageName, - label = it.applicationInfo?.loadLabel(packageManager).toString(), - isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1, - lastUpdateTime = it.lastUpdateTime - ) - }?.let { packages.addAll(it) } + Package( + packageName = it.packageName, + label = it.applicationInfo?.loadLabel(packageManager).toString(), + system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1, + lastUpdateTime = it.lastUpdateTime, + internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + ) + }?.let { packages.addAll(it) } return packages } diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt index e316110..5ae81c1 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt @@ -168,8 +168,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { try { if (GlobalState.runState.value != RunState.START) return val data = flutterMethodChannel.awaitResult("getStartForegroundParams") - val startForegroundParams = Gson().fromJson( + val startForegroundParams = if (data != null) Gson().fromJson( data, StartForegroundParams::class.java + ) else StartForegroundParams( + title = "", content = "" ) if (lastStartForegroundParams != startForegroundParams) { lastStartForegroundParams = startForegroundParams diff --git a/android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt b/android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt index 44ca809..5a8bd72 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/BaseServiceInterface.kt @@ -1,6 +1,26 @@ package com.follow.clash.services +import android.annotation.SuppressLint +import android.app.Notification +import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.os.Build +import androidx.core.app.NotificationCompat +import com.follow.clash.GlobalState +import com.follow.clash.MainActivity +import com.follow.clash.R +import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.models.VpnOptions +import io.flutter.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async interface BaseServiceInterface { @@ -9,4 +29,70 @@ interface BaseServiceInterface { fun stop() suspend fun startForeground(title: String, content: String) +} + +fun Service.createFlClashNotificationBuilder(): Deferred = + CoroutineScope(Dispatchers.Main).async { + val stopText = GlobalState.getText("stop") + val intent = Intent(this@createFlClashNotificationBuilder, MainActivity::class.java) + + val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { + PendingIntent.getActivity( + this@createFlClashNotificationBuilder, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } else { + PendingIntent.getActivity( + this@createFlClashNotificationBuilder, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + with( + NotificationCompat.Builder( + this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL + ) + ) { + setSmallIcon(R.drawable.ic_stat_name) + setContentTitle("FlClash") + setContentIntent(pendingIntent) + setCategory(NotificationCompat.CATEGORY_SERVICE) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE + } + setOngoing(true) + addAction( + 0, stopText, getActionPendingIntent("STOP") + ) + setShowWhen(false) + setOnlyAlertOnce(true) + } + } + +@SuppressLint("ForegroundServiceType") +fun Service.startForeground(notification: Notification) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = getSystemService(NotificationManager::class.java) + var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL) + if (channel == null) { + Log.d("[FlClash]","createNotificationChannel===>") + channel = NotificationChannel( + GlobalState.NOTIFICATION_CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW + ) + manager?.createNotificationChannel(channel) + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + try { + startForeground( + GlobalState.NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } catch (_: Exception) { + startForeground(GlobalState.NOTIFICATION_ID, notification) + } + } else { + startForeground(GlobalState.NOTIFICATION_ID, notification) + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt index fa0ba54..2f09531 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt @@ -1,29 +1,51 @@ package com.follow.clash.services import android.annotation.SuppressLint -import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent import android.app.Service import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Binder import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import com.follow.clash.GlobalState -import com.follow.clash.MainActivity -import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.models.VpnOptions -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async class FlClashService : Service(), BaseServiceInterface { + override fun start(options: VpnOptions) = 0 + + override fun stop() { + stopSelf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + stopForeground(STOP_FOREGROUND_REMOVE) + } + } + + private var cachedBuilder: NotificationCompat.Builder? = null + + private suspend fun notificationBuilder(): NotificationCompat.Builder { + if (cachedBuilder == null) { + cachedBuilder = createFlClashNotificationBuilder().await() + } + return cachedBuilder!! + } + + @SuppressLint("ForegroundServiceType") + override suspend fun startForeground(title: String, content: String) { + startForeground( + notificationBuilder() + .setContentTitle(title) + .setContentText(content).build() + ) + } + + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + GlobalState.getCurrentVPNPlugin()?.requestGc() + } + + private val binder = LocalBinder() inner class LocalBinder : Binder() { @@ -38,93 +60,8 @@ class FlClashService : Service(), BaseServiceInterface { return super.onUnbind(intent) } - private val CHANNEL = "FlClash" - - private val notificationId: Int = 1 - - private val notificationBuilderDeferred: Deferred by lazy { - CoroutineScope(Dispatchers.Main).async { - val stopText = GlobalState.getText("stop") - - val intent = Intent( - this@FlClashService, MainActivity::class.java - ) - - val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { - PendingIntent.getActivity( - this@FlClashService, - 0, - intent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - } else { - PendingIntent.getActivity( - this@FlClashService, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - } - - with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) { - setSmallIcon(com.follow.clash.R.drawable.ic_stat_name) - setContentTitle("FlClash") - setContentIntent(pendingIntent) - setCategory(NotificationCompat.CATEGORY_SERVICE) - priority = NotificationCompat.PRIORITY_MIN - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE - } - addAction( - 0, - stopText, // 使用 suspend 函数获取的文本 - getActionPendingIntent("STOP") - ) - setOngoing(true) - setShowWhen(false) - setOnlyAlertOnce(true) - setAutoCancel(true) - } - } - } - - private suspend fun getNotificationBuilder(): NotificationCompat.Builder { - return notificationBuilderDeferred.await() - } - - override fun start(options: VpnOptions) = 0 - - override fun stop() { - stopSelf() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - stopForeground(STOP_FOREGROUND_REMOVE) - } - } - - - @SuppressLint("ForegroundServiceType") - override suspend fun startForeground(title: String, content: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = getSystemService(NotificationManager::class.java) - var channel = manager?.getNotificationChannel(CHANNEL) - if (channel == null) { - channel = - NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) - manager?.createNotificationChannel(channel) - } - } - val notification = - getNotificationBuilder() - .setContentTitle(title) - .setContentText(content).build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - try { - startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC) - } catch (_: Exception) { - startForeground(notificationId, notification) - } - } else { - startForeground(notificationId, notification) - } + override fun onDestroy() { + stop() + super.onDestroy() } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt index 37d39a0..cb6ab40 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt @@ -1,12 +1,7 @@ package com.follow.clash.services import android.annotation.SuppressLint -import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.net.ProxyInfo import android.net.VpnService import android.os.Binder @@ -17,18 +12,13 @@ import android.os.RemoteException import android.util.Log import androidx.core.app.NotificationCompat import com.follow.clash.GlobalState -import com.follow.clash.MainActivity -import com.follow.clash.R -import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.extensions.getIpv4RouteAddress import com.follow.clash.extensions.getIpv6RouteAddress import com.follow.clash.extensions.toCIDR import com.follow.clash.models.AccessControlMode import com.follow.clash.models.VpnOptions import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.launch @@ -43,6 +33,10 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { if (options.ipv4Address.isNotEmpty()) { val cidr = options.ipv4Address.toCIDR() addAddress(cidr.address, cidr.prefixLength) + Log.d( + "addAddress", + "address: ${cidr.address} prefixLength:${cidr.prefixLength}" + ) val routeAddress = options.getIpv4RouteAddress() if (routeAddress.isNotEmpty()) { try { @@ -59,26 +53,39 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { } else { addRoute("0.0.0.0", 0) } + } else { + addRoute("0.0.0.0", 0) } - if (options.ipv6Address.isNotEmpty()) { - val cidr = options.ipv6Address.toCIDR() - addAddress(cidr.address, cidr.prefixLength) - val routeAddress = options.getIpv6RouteAddress() - if (routeAddress.isNotEmpty()) { - try { - routeAddress.forEach { i -> - Log.d( - "addRoute6", - "address: ${i.address} prefixLength:${i.prefixLength}" - ) - addRoute(i.address, i.prefixLength) + try { + if (options.ipv6Address.isNotEmpty()) { + val cidr = options.ipv6Address.toCIDR() + Log.d( + "addAddress6", + "address: ${cidr.address} prefixLength:${cidr.prefixLength}" + ) + addAddress(cidr.address, cidr.prefixLength) + val routeAddress = options.getIpv6RouteAddress() + if (routeAddress.isNotEmpty()) { + try { + routeAddress.forEach { i -> + Log.d( + "addRoute6", + "address: ${i.address} prefixLength:${i.prefixLength}" + ) + addRoute(i.address, i.prefixLength) + } + } catch (_: Exception) { + addRoute("::", 0) } - } catch (_: Exception) { + } else { addRoute("::", 0) } - } else { - addRoute("::", 0) } + }catch (_:Exception){ + Log.d( + "addAddress6", + "IPv6 is not supported." + ) } addDnsServer(options.dnsServerAddress) setMtu(9000) @@ -128,82 +135,22 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { } } - private val CHANNEL = "FlClash" + private var cachedBuilder: NotificationCompat.Builder? = null - private val notificationId: Int = 1 - - private val notificationBuilderDeferred: Deferred by lazy { - CoroutineScope(Dispatchers.Main).async { - val stopText = GlobalState.getText("stop") - val intent = Intent(this@FlClashVpnService, MainActivity::class.java) - - val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { - PendingIntent.getActivity( - this@FlClashVpnService, - 0, - intent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - } else { - PendingIntent.getActivity( - this@FlClashVpnService, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - } - - with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) { - setSmallIcon(R.drawable.ic_stat_name) - setContentTitle("FlClash") - setContentIntent(pendingIntent) - setCategory(NotificationCompat.CATEGORY_SERVICE) - priority = NotificationCompat.PRIORITY_MIN - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE - } - setOngoing(true) - addAction( - 0, - stopText, - getActionPendingIntent("STOP") - ) - setShowWhen(false) - setOnlyAlertOnce(true) - setAutoCancel(true) - } + private suspend fun notificationBuilder(): NotificationCompat.Builder { + if (cachedBuilder == null) { + cachedBuilder = createFlClashNotificationBuilder().await() } - } - - private suspend fun getNotificationBuilder(): NotificationCompat.Builder { - return notificationBuilderDeferred.await() + return cachedBuilder!! } @SuppressLint("ForegroundServiceType") override suspend fun startForeground(title: String, content: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = getSystemService(NotificationManager::class.java) - var channel = manager?.getNotificationChannel(CHANNEL) - if (channel == null) { - channel = - NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) - manager?.createNotificationChannel(channel) - } - } - val notification = - getNotificationBuilder() + startForeground( + notificationBuilder() .setContentTitle(title) - .setContentText(content) - .build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - try { - startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC) - } catch (_: Exception) { - startForeground(notificationId, notification) - } - } else { - startForeground(notificationId, notification) - } + .setContentText(content).build() + ) } override fun onTrimMemory(level: Int) { diff --git a/android/core/build.gradle.kts b/android/core/build.gradle.kts index 63c5e41..6c1404e 100644 --- a/android/core/build.gradle.kts +++ b/android/core/build.gradle.kts @@ -1,5 +1,3 @@ -import com.android.build.gradle.tasks.MergeSourceSetFolders - plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -37,13 +35,17 @@ android { } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} +dependencies { + implementation("androidx.annotation:annotation-jvm:1.9.1") } val copyNativeLibs by tasks.register("copyNativeLibs") { @@ -58,8 +60,4 @@ afterEvaluate { tasks.named("preBuild") { dependsOn(copyNativeLibs) } -} - -dependencies { - implementation("androidx.core:core-ktx:1.16.0") } \ No newline at end of file diff --git a/android/core/src/main/cpp/CMakeLists.txt b/android/core/src/main/cpp/CMakeLists.txt index 0246d86..7c29f76 100644 --- a/android/core/src/main/cpp/CMakeLists.txt +++ b/android/core/src/main/cpp/CMakeLists.txt @@ -21,6 +21,8 @@ if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") -Wl,--strip-all -Wl,--exclude-libs=ALL ) + + add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden) endif () set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so") diff --git a/lib/l10n/arb/intl_en.arb b/arb/intl_en.arb similarity index 96% rename from lib/l10n/arb/intl_en.arb rename to arb/intl_en.arb index 0d8f281..bbf21f1 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/arb/intl_en.arb @@ -385,5 +385,20 @@ "expressiveScheme": "Expressive", "contentScheme": "Content", "rainbowScheme": "Rainbow", - "fruitSaladScheme": "FruitSalad" + "fruitSaladScheme": "FruitSalad", + "developerMode": "Developer mode", + "developerModeEnableTip": "Developer mode is enabled.", + "messageTest": "Message test", + "messageTestTip": "This is a message.", + "crashTest": "Crash test", + "clearData": "Clear Data", + "textScale": "Text Scaling", + "internet": "Internet", + "systemApp": "System APP", + "noNetworkApp": "No network APP", + "contactMe": "Contact me", + "recoveryStrategy": "Recovery strategy", + "recoveryStrategy_override": "Override", + "recoveryStrategy_compatible": "Compatible", + "logsTest": "Logs test" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_ja.arb b/arb/intl_ja.arb similarity index 95% rename from lib/l10n/arb/intl_ja.arb rename to arb/intl_ja.arb index 7282952..c3093a9 100644 --- a/lib/l10n/arb/intl_ja.arb +++ b/arb/intl_ja.arb @@ -385,5 +385,21 @@ "expressiveScheme": "エクスプレッシブ", "contentScheme": "コンテンツテーマ", "rainbowScheme": "レインボー", - "fruitSaladScheme": "フルーツサラダ" + "fruitSaladScheme": "フルーツサラダ", + "developerMode": "デベロッパーモード", + "developerModeEnableTip": "デベロッパーモードが有効になりました。", + "messageTest": "メッセージテスト", + "messageTestTip": "これはメッセージです。", + "crashTest": "クラッシュテスト", + "clearData": "データを消去", + "zoom": "ズーム", + "textScale": "テキストスケーリング", + "internet": "インターネット", + "systemApp": "システムアプリ", + "noNetworkApp": "ネットワークなしアプリ", + "contactMe": "連絡する", + "recoveryStrategy": "リカバリー戦略", + "recoveryStrategy_override": "オーバーライド", + "recoveryStrategy_compatible": "互換性", + "logsTest": "ログテスト" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_ru.arb b/arb/intl_ru.arb similarity index 96% rename from lib/l10n/arb/intl_ru.arb rename to arb/intl_ru.arb index 132ec01..31ad5a2 100644 --- a/lib/l10n/arb/intl_ru.arb +++ b/arb/intl_ru.arb @@ -385,5 +385,21 @@ "expressiveScheme": "Экспрессивные", "contentScheme": "Контентная тема", "rainbowScheme": "Радужные", - "fruitSaladScheme": "Фруктовый микс" + "fruitSaladScheme": "Фруктовый микс", + "developerMode": "Режим разработчика", + "developerModeEnableTip": "Режим разработчика активирован.", + "messageTest": "Тестирование сообщения", + "messageTestTip": "Это сообщение.", + "crashTest": "Тест на сбои", + "clearData": "Очистить данные", + "zoom": "Масштаб", + "textScale": "Масштабирование текста", + "internet": "Интернет", + "systemApp": "Системное приложение", + "noNetworkApp": "Приложение без сети", + "contactMe": "Свяжитесь со мной", + "recoveryStrategy": "Стратегия восстановления", + "recoveryStrategy_override": "Переопределение", + "recoveryStrategy_compatible": "Совместимый", + "logsTest": "Тест журналов" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/arb/intl_zh_CN.arb similarity index 95% rename from lib/l10n/arb/intl_zh_CN.arb rename to arb/intl_zh_CN.arb index 00a7749..3b7e279 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/arb/intl_zh_CN.arb @@ -385,5 +385,21 @@ "expressiveScheme": "表现力", "contentScheme": "内容主题", "rainbowScheme": "彩虹", - "fruitSaladScheme": "果缤纷" + "fruitSaladScheme": "果缤纷", + "developerMode": "开发者模式", + "developerModeEnableTip": "开发者模式已启用。", + "messageTest": "消息测试", + "messageTestTip": "这是一条消息。", + "crashTest": "崩溃测试", + "clearData": "清除数据", + "zoom": "缩放", + "textScale": "文本缩放", + "internet": "互联网", + "systemApp": "系统应用", + "noNetworkApp": "无网络应用", + "contactMe": "联系我", + "recoveryStrategy": "恢复策略", + "recoveryStrategy_override": "覆盖", + "recoveryStrategy_compatible": "兼容", + "logsTest": "日志测试" } diff --git a/core/Clash.Meta b/core/Clash.Meta index f19dad5..88a1848 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit f19dad529f7d8ac652053f9d090e6780e199eab2 +Subproject commit 88a1848dfbdf2fda2ca50f90cda145887aaaaae4 diff --git a/core/action.go b/core/action.go index d170124..def5cd1 100644 --- a/core/action.go +++ b/core/action.go @@ -166,6 +166,9 @@ func handleAction(action *Action, result func(data interface{})) { data := action.Data.(string) handleSetState(data) result(true) + case crashMethod: + result(true) + handleCrash() default: handle := nextHandle(action, result) if handle { diff --git a/core/common.go b/core/common.go index 2076bd2..506ebb5 100644 --- a/core/common.go +++ b/core/common.go @@ -78,7 +78,6 @@ func getRawConfigWithId(id string) *config.RawConfig { path := getProfilePath(id) bytes, err := readFile(path) if err != nil { - log.Errorln("profile is not exist") return config.DefaultRawConfig() } prof, err := config.UnmarshalRawConfig(bytes) diff --git a/core/constant.go b/core/constant.go index 1da3e33..f3a9ab9 100644 --- a/core/constant.go +++ b/core/constant.go @@ -82,6 +82,7 @@ const ( getRunTimeMethod Method = "getRunTime" getCurrentProfileNameMethod Method = "getCurrentProfileName" getProfileMethod Method = "getProfile" + crashMethod Method = "crash" ) type Method string diff --git a/core/go.mod b/core/go.mod index aeb042f..d7a5220 100644 --- a/core/go.mod +++ b/core/go.mod @@ -7,6 +7,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta require ( github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 github.com/samber/lo v1.49.1 + golang.org/x/sync v0.11.0 ) require ( @@ -52,20 +53,20 @@ require ( github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect github.com/metacubex/bart v0.19.0 // indirect github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect - github.com/metacubex/chacha v0.1.1 // indirect + github.com/metacubex/chacha v0.1.2 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect github.com/metacubex/randv2 v0.2.0 // indirect - github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect - github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect + github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c // indirect github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect - github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect + github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 // indirect + github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 // indirect github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect - github.com/metacubex/utls v1.6.8-alpha.4 // indirect + github.com/metacubex/utls v1.7.0-alpha.1 // indirect github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect github.com/miekg/dns v1.1.63 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect @@ -84,7 +85,6 @@ require ( github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/sing v0.5.2 // indirect github.com/sagernet/sing-mux v0.2.1 // indirect - github.com/sagernet/sing-shadowtls v0.1.5 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect @@ -107,7 +107,6 @@ require ( golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.35.0 // indirect - golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.7.0 // indirect diff --git a/core/go.sum b/core/go.sum index 4a7fccb..ebec92d 100644 --- a/core/go.sum +++ b/core/go.sum @@ -101,8 +101,8 @@ github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= -github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c= -github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= +github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc= +github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= @@ -111,24 +111,24 @@ github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/j github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= -github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs= -github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8= -github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA= -github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= +github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c h1:OB3WmMA8YPJjE36RjD9X8xlrWGJ4orxbf2R/KAE28b0= +github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= -github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg= -github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= +github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 h1:vy/8ZYYtWUXYnOnw/NF8ThG1W/RqM/h5rkun+OXZMH0= +github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63/go.mod h1:eDZ2JpkSkewGmUlCoLSn2MRFn1D0jKPIys/6aogFx7U= +github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 h1:hcsz5e5lqhBxn3iQQDIF60FLZ8PQT542GTQZ+1wcIGo= +github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= -github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI= -github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk= +github.com/metacubex/utls v1.7.0-alpha.1 h1:oMFsPh2oTlALJ7vKXPJuqgy0YeiZ+q/LLw+ZdxZ80l4= +github.com/metacubex/utls v1.7.0-alpha.1/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= @@ -174,8 +174,6 @@ github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M= github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo= github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= -github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo= -github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= diff --git a/core/hub.go b/core/hub.go index e92c374..06095e9 100644 --- a/core/hub.go +++ b/core/hub.go @@ -442,6 +442,10 @@ func handleSetState(params string) { _ = json.Unmarshal([]byte(params), state.CurrentState) } +func handleCrash() { + panic("handle invoke crash") +} + func init() { adapter.UrlTestHook = func(url string, name string, delay uint16) { delayData := &Delay{ diff --git a/core/lib_android.go b/core/lib_android.go index 97587de..9ed8541 100644 --- a/core/lib_android.go +++ b/core/lib_android.go @@ -98,13 +98,13 @@ func handleStopTun() { } } -func handleStartTun(fd int, callback unsafe.Pointer) bool { +func handleStartTun(fd int, callback unsafe.Pointer) { handleStopTun() + tunLock.Lock() + defer tunLock.Unlock() now := time.Now() runTime = &now if fd != 0 { - tunLock.Lock() - defer tunLock.Unlock() tunHandler = &TunHandler{ callback: callback, limit: semaphore.NewWeighted(4), @@ -113,13 +113,11 @@ func handleStartTun(fd int, callback unsafe.Pointer) bool { tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack) if tunListener != nil { log.Infoln("TUN address: %v", tunListener.Address()) + tunHandler.listener = tunListener } else { removeTunHook() - return false } - tunHandler.listener = tunListener } - return true } func handleGetRunTime() string { @@ -228,7 +226,10 @@ func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.c //export startTUN func startTUN(fd C.int, callback unsafe.Pointer) bool { - return handleStartTun(int(fd), callback) + go func() { + handleStartTun(int(fd), callback) + }() + return true } //export getRunTime @@ -238,7 +239,9 @@ func getRunTime() *C.char { //export stopTun func stopTun() { - handleStopTun() + go func() { + handleStopTun() + }() } //export getCurrentProfileName diff --git a/core/state/state.go b/core/state/state.go index 9bbb6ca..62beaf5 100644 --- a/core/state/state.go +++ b/core/state/state.go @@ -24,7 +24,6 @@ type AccessControl struct { Mode string `json:"mode"` AcceptList []string `json:"acceptList"` RejectList []string `json:"rejectList"` - IsFilterSystemApp bool `json:"isFilterSystemApp"` } type AndroidVpnRawOptions struct { diff --git a/lib/application.dart b/lib/application.dart index 1a3027d..0f62f2d 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/l10n/l10n.dart'; @@ -14,7 +13,6 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'controller.dart'; -import 'models/models.dart'; import 'pages/pages.dart'; class Application extends ConsumerStatefulWidget { @@ -27,7 +25,6 @@ class Application extends ConsumerStatefulWidget { } class ApplicationState extends ConsumerState { - late ColorSchemes systemColorSchemes; Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateProfilesTaskTimer; @@ -132,19 +129,6 @@ class ApplicationState extends ConsumerState { ); } - _updateSystemColorSchemes( - ColorScheme? lightDynamic, - ColorScheme? darkDynamic, - ) { - systemColorSchemes = ColorSchemes( - lightColorScheme: lightDynamic, - darkColorScheme: darkDynamic, - ); - WidgetsBinding.instance.addPostFrameCallback((_) { - globalState.appController.updateSystemColorSchemes(systemColorSchemes); - }); - } - @override Widget build(context) { return _buildPlatformState( @@ -154,49 +138,44 @@ class ApplicationState extends ConsumerState { final locale = ref.watch(appSettingProvider.select((state) => state.locale)); final themeProps = ref.watch(themeSettingProvider); - return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - _updateSystemColorSchemes(lightDynamic, darkDynamic); - return MaterialApp( - debugShowCheckedModeBanner: false, - navigatorKey: globalState.navigatorKey, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate - ], - builder: (_, child) { - return AppEnvManager( - child: _buildPlatformApp( - _buildApp(child!), - ), - ); - }, - scrollBehavior: BaseScrollBehavior(), - title: appName, - locale: utils.getLocaleForString(locale), - supportedLocales: AppLocalizations.delegate.supportedLocales, - themeMode: themeProps.themeMode, - theme: ThemeData( - useMaterial3: true, - pageTransitionsTheme: _pageTransitionsTheme, - colorScheme: _getAppColorScheme( - brightness: Brightness.light, - primaryColor: themeProps.primaryColor, - ), + return MaterialApp( + debugShowCheckedModeBanner: false, + navigatorKey: globalState.navigatorKey, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate + ], + builder: (_, child) { + return AppEnvManager( + child: _buildPlatformApp( + _buildApp(child!), ), - darkTheme: ThemeData( - useMaterial3: true, - pageTransitionsTheme: _pageTransitionsTheme, - colorScheme: _getAppColorScheme( - brightness: Brightness.dark, - primaryColor: themeProps.primaryColor, - ).toPureBlack(themeProps.pureBlack), - ), - home: child, ); }, + scrollBehavior: BaseScrollBehavior(), + title: appName, + locale: utils.getLocaleForString(locale), + supportedLocales: AppLocalizations.delegate.supportedLocales, + themeMode: themeProps.themeMode, + theme: ThemeData( + useMaterial3: true, + pageTransitionsTheme: _pageTransitionsTheme, + colorScheme: _getAppColorScheme( + brightness: Brightness.light, + primaryColor: themeProps.primaryColor, + ), + ), + darkTheme: ThemeData( + useMaterial3: true, + pageTransitionsTheme: _pageTransitionsTheme, + colorScheme: _getAppColorScheme( + brightness: Brightness.dark, + primaryColor: themeProps.primaryColor, + ).toPureBlack(themeProps.pureBlack), + ), + home: child, ); }, child: const HomePage(), diff --git a/lib/clash/interface.dart b/lib/clash/interface.dart index b0f11ea..c0ee9de 100644 --- a/lib/clash/interface.dart +++ b/lib/clash/interface.dart @@ -58,6 +58,8 @@ mixin ClashInterface { stopLog(); + Future crash(); + FutureOr getConnections(); FutureOr closeConnection(String id); @@ -104,6 +106,7 @@ abstract class ClashHandlerInterface with ClashInterface { case ActionMethod.closeConnection: case ActionMethod.stopListener: case ActionMethod.setState: + case ActionMethod.crash: completer?.complete(result.data as bool); return; case ActionMethod.changeProxy: @@ -242,6 +245,13 @@ abstract class ClashHandlerInterface with ClashInterface { ); } + @override + Future crash() { + return invoke( + method: ActionMethod.crash, + ); + } + @override Future getProxies() { return invoke( diff --git a/lib/clash/service.dart b/lib/clash/service.dart index 5723b41..2c1866c 100644 --- a/lib/clash/service.dart +++ b/lib/clash/service.dart @@ -71,9 +71,9 @@ class ClashService extends ClashHandlerInterface { } }, (error, stack) { commonPrint.log(error.toString()); - if(error is SocketException){ + if (error is SocketException) { globalState.showNotifier(error.toString()); - globalState.appController.restartCore(); + // globalState.appController.restartCore(); } }); } @@ -92,12 +92,11 @@ class ClashService extends ClashHandlerInterface { final arg = Platform.isWindows ? "${serverSocket.port}" : serverSocket.address.address; - bool isSuccess = false; if (Platform.isWindows && await system.checkIsAdmin()) { - isSuccess = await request.startCoreByHelper(arg); - } - if (isSuccess) { - return; + final isSuccess = await request.startCoreByHelper(arg); + if (isSuccess) { + return; + } } process = await Process.start( appPath.corePath, diff --git a/lib/common/common.dart b/lib/common/common.dart index 1b95ccf..7aae94d 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -12,7 +12,7 @@ export 'iterable.dart'; export 'keyboard.dart'; export 'launch.dart'; export 'link.dart'; -export 'list.dart'; +export 'fixed.dart'; export 'lock.dart'; export 'measure.dart'; export 'navigation.dart'; diff --git a/lib/common/constant.dart b/lib/common/constant.dart index 907cd7d..908cb8a 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -16,16 +16,14 @@ const browserUa = const packageName = "com.follow.clash"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; const helperPort = 47890; -const helperTag = "2024125"; -const baseInfoEdgeInsets = EdgeInsets.symmetric( - vertical: 16, - horizontal: 16, +const maxTextScale = 1.4; +const minTextScale = 0.8; +final baseInfoEdgeInsets = EdgeInsets.symmetric( + vertical: 16.ap, + horizontal: 16.ap, ); -double textScaleFactor = min( - WidgetsBinding.instance.platformDispatcher.textScaleFactor, - 1.2, -); +final defaultTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor; const httpTimeoutDuration = Duration(milliseconds: 5000); const moreDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100); @@ -44,7 +42,6 @@ const profilesDirectoryName = "profiles"; const localhost = "127.0.0.1"; const clashConfigKey = "clash_config"; const configKey = "config"; -const listItemPadding = EdgeInsets.symmetric(horizontal: 16); const double dialogCommonWidth = 300; const repository = "chen08209/FlClash"; const defaultExternalController = "127.0.0.1:9090"; @@ -60,6 +57,7 @@ final commonFilter = ImageFilter.blur( const navigationItemListEquality = ListEquality(); const connectionListEquality = ListEquality(); const stringListEquality = ListEquality(); +const intListEquality = ListEquality(); const logListEquality = ListEquality(); const groupListEquality = ListEquality(); const externalProviderListEquality = ListEquality(); @@ -78,22 +76,24 @@ const viewModeColumnsMap = { ViewMode.desktop: [4, 3], }; -const defaultPrimaryColor = 0xFF795548; +const defaultPrimaryColor = 0XFFD8C0C3; double getWidgetHeight(num lines) { - return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0); + return max(lines * 84 + (lines - 1) * 16, 0).ap; } +const maxLength = 150; + final mainIsolate = "FlClashMainIsolate"; final serviceIsolate = "FlClashServiceIsolate"; const defaultPrimaryColors = [ - defaultPrimaryColor, + 0xFF795548, 0xFF03A9F4, 0xFFFFFF00, 0XFFBBC9CC, 0XFFABD397, - 0XFFD8C0C3, + defaultPrimaryColor, 0XFF665390, ]; diff --git a/lib/common/context.dart b/lib/common/context.dart index 47e364d..70751b9 100644 --- a/lib/common/context.dart +++ b/lib/common/context.dart @@ -1,4 +1,4 @@ -import 'package:fl_clash/manager/manager.dart'; +import 'package:fl_clash/manager/message_manager.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,36 @@ extension BuildContextExtension on BuildContext { return findAncestorStateOfType()?.message(text); } + showSnackBar( + String message, { + SnackBarAction? action, + }) { + final width = viewWidth; + EdgeInsets margin; + if (width < 600) { + margin = const EdgeInsets.only( + bottom: 16, + right: 16, + left: 16, + ); + } else { + margin = EdgeInsets.only( + bottom: 16, + left: 16, + right: width - 316, + ); + } + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + action: action, + content: Text(message), + behavior: SnackBarBehavior.floating, + duration: const Duration(milliseconds: 1500), + margin: margin, + ), + ); + } + Size get appSize { return MediaQuery.of(this).size; } @@ -27,10 +57,10 @@ extension BuildContextExtension on BuildContext { T? state; visitor(Element element) { - if(!element.mounted){ + if (!element.mounted) { return; } - if(element is StatefulElement){ + if (element is StatefulElement) { if (element.state is T) { state = element.state as T; } diff --git a/lib/common/fixed.dart b/lib/common/fixed.dart new file mode 100644 index 0000000..55e8ef0 --- /dev/null +++ b/lib/common/fixed.dart @@ -0,0 +1,79 @@ +import 'iterable.dart'; + +class FixedList { + final int maxLength; + final List _list; + + FixedList(this.maxLength, {List? list}) + : _list = (list ?? [])..truncate(maxLength); + + add(T item) { + _list.add(item); + _list.truncate(maxLength); + } + + clear() { + _list.clear(); + } + + List get list => List.unmodifiable(_list); + + int get length => _list.length; + + T operator [](int index) => _list[index]; + + FixedList copyWith() { + return FixedList( + maxLength, + list: _list, + ); + } +} + +class FixedMap { + int maxLength; + late Map _map; + + FixedMap(this.maxLength, {Map? map}) { + _map = map ?? {}; + } + + updateCacheValue(K key, V Function() callback) { + final realValue = _map.updateCacheValue( + key, + callback, + ); + _adjustMap(); + return realValue; + } + + clear() { + _map.clear(); + } + + updateMaxLength(int size) { + maxLength = size; + _adjustMap(); + } + + updateMap(Map map) { + _map = map; + _adjustMap(); + } + + _adjustMap() { + if (_map.length > maxLength) { + _map = Map.fromEntries( + map.entries.toList()..truncate(maxLength), + ); + } + } + + V? get(K key) => _map[key]; + + bool containsKey(K key) => _map.containsKey(key); + + int get length => _map.length; + + Map get map => Map.unmodifiable(_map); +} diff --git a/lib/common/iterable.dart b/lib/common/iterable.dart index aac4471..9cd7033 100644 --- a/lib/common/iterable.dart +++ b/lib/common/iterable.dart @@ -38,6 +38,43 @@ extension IterableExt on Iterable { count++; } } + + Iterable takeLast({int count = 50}) { + if (count <= 0) return Iterable.empty(); + return count >= length ? this : toList().skip(length - count); + } +} + +extension ListExt on List { + void truncate(int maxLength) { + assert(maxLength > 0); + if (length > maxLength) { + removeRange(0, length - maxLength); + } + } + + List intersection(List list) { + return where((item) => list.contains(item)).toList(); + } + + List> batch(int maxConcurrent) { + final batches = (length / maxConcurrent).ceil(); + final List> res = []; + for (int i = 0; i < batches; i++) { + if (i != batches - 1) { + res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1))); + } else { + res.add(sublist(i * maxConcurrent, length)); + } + } + return res; + } + + List safeSublist(int start) { + if (start <= 0) return this; + if (start > length) return []; + return sublist(start); + } } extension DoubleListExt on List { @@ -67,9 +104,9 @@ extension DoubleListExt on List { } extension MapExt on Map { - getCacheValue(K key, V defaultValue) { + updateCacheValue(K key, V Function() callback) { if (this[key] == null) { - this[key] = defaultValue; + this[key] = callback(); } return this[key]; } diff --git a/lib/common/list.dart b/lib/common/list.dart deleted file mode 100644 index ca8add7..0000000 --- a/lib/common/list.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:collection'; - -class FixedList { - final int maxLength; - final List _list; - - FixedList(this.maxLength, {List? list}) : _list = list ?? []; - - add(T item) { - if (_list.length == maxLength) { - _list.removeAt(0); - } - _list.add(item); - } - - clear() { - _list.clear(); - } - - List get list => List.unmodifiable(_list); - - int get length => _list.length; - - T operator [](int index) => _list[index]; - - FixedList copyWith() { - return FixedList( - maxLength, - list: _list, - ); - } -} - -class FixedMap { - int maxSize; - final Map _map = {}; - final Queue _queue = Queue(); - - FixedMap(this.maxSize); - - put(K key, V value) { - if (_map.length == maxSize) { - final oldestKey = _queue.removeFirst(); - _map.remove(oldestKey); - } - _map[key] = value; - _queue.add(key); - return value; - } - - clear() { - _map.clear(); - _queue.clear(); - } - - updateMaxSize(int size){ - maxSize = size; - } - - V? get(K key) => _map[key]; - - - bool containsKey(K key) => _map.containsKey(key); - - int get length => _map.length; - - Map get map => Map.unmodifiable(_map); -} - -extension ListExtension on List { - List intersection(List list) { - return where((item) => list.contains(item)).toList(); - } - - List> batch(int maxConcurrent) { - final batches = (length / maxConcurrent).ceil(); - final List> res = []; - for (int i = 0; i < batches; i++) { - if (i != batches - 1) { - res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1))); - } else { - res.add(sublist(i * maxConcurrent, length)); - } - } - return res; - } - - List safeSublist(int start) { - if (start <= 0) return this; - if (start > length) return []; - return sublist(start); - } -} diff --git a/lib/common/measure.dart b/lib/common/measure.dart index 736fdec..fbeabf5 100644 --- a/lib/common/measure.dart +++ b/lib/common/measure.dart @@ -3,11 +3,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class Measure { - final TextScaler _textScale; + final TextScaler _textScaler; final BuildContext context; + final Map _measureMap; - Measure.of(this.context) - : _textScale = TextScaler.linear( + Measure.of(this.context, double textScaleFactor) + : _measureMap = {}, + _textScaler = TextScaler.linear( textScaleFactor, ); @@ -21,7 +23,7 @@ class Measure { style: text.style, ), maxLines: text.maxLines, - textScaler: _textScale, + textScaler: _textScaler, textDirection: text.textDirection ?? TextDirection.ltr, )..layout( maxWidth: maxWidth, @@ -29,81 +31,87 @@ class Measure { return textPainter.size; } - double? _bodyMediumHeight; - Size? _bodyLargeSize; - double? _bodySmallHeight; - double? _labelSmallHeight; - double? _labelMediumHeight; - double? _titleLargeHeight; - double? _titleMediumHeight; - double get bodyMediumHeight { - _bodyMediumHeight ??= computeTextSize( - Text( - "X", - style: context.textTheme.bodyMedium, - ), - ).height; - return _bodyMediumHeight!; + return _measureMap.updateCacheValue( + "bodyMediumHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.bodyMedium, + ), + ).height, + ); } - Size get bodyLargeSize { - _bodyLargeSize ??= computeTextSize( - Text( - "X", - style: context.textTheme.bodyLarge, - ), + double get bodyLargeHeight { + return _measureMap.updateCacheValue( + "bodyLargeHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.bodyLarge, + ), + ).height, ); - return _bodyLargeSize!; } double get bodySmallHeight { - _bodySmallHeight ??= computeTextSize( - Text( - "X", - style: context.textTheme.bodySmall, - ), - ).height; - return _bodySmallHeight!; + return _measureMap.updateCacheValue( + "bodySmallHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.bodySmall, + ), + ).height, + ); } double get labelSmallHeight { - _labelSmallHeight ??= computeTextSize( - Text( - "X", - style: context.textTheme.labelSmall, - ), - ).height; - return _labelSmallHeight!; + return _measureMap.updateCacheValue( + "labelSmallHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.labelSmall, + ), + ).height, + ); } double get labelMediumHeight { - _labelMediumHeight ??= computeTextSize( - Text( - "X", - style: context.textTheme.labelMedium, - ), - ).height; - return _labelMediumHeight!; + return _measureMap.updateCacheValue( + "labelMediumHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.labelMedium, + ), + ).height, + ); } double get titleLargeHeight { - _titleLargeHeight ??= computeTextSize( - Text( - "X", - style: context.textTheme.titleLarge, - ), - ).height; - return _titleLargeHeight!; + return _measureMap.updateCacheValue( + "titleLargeHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.titleLarge, + ), + ).height, + ); } double get titleMediumHeight { - _titleMediumHeight ??= computeTextSize( - Text( - "X", - style: context.textTheme.titleMedium, - ), - ).height; - return _titleMediumHeight!; + return _measureMap.updateCacheValue( + "titleMediumHeight", + () => computeTextSize( + Text( + "X", + style: context.textTheme.titleMedium, + ), + ).height, + ); } } diff --git a/lib/common/navigation.dart b/lib/common/navigation.dart index efe512c..5d2ccab 100644 --- a/lib/common/navigation.dart +++ b/lib/common/navigation.dart @@ -14,7 +14,6 @@ class Navigation { const NavigationItem( icon: Icon(Icons.space_dashboard), label: PageLabel.dashboard, - keep: false, fragment: DashboardFragment( key: GlobalObjectKey(PageLabel.dashboard), ), diff --git a/lib/common/num.dart b/lib/common/num.dart index b9503b7..7166509 100644 --- a/lib/common/num.dart +++ b/lib/common/num.dart @@ -1,3 +1,4 @@ +import 'package:fl_clash/state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,6 +13,10 @@ extension NumExt on num { } return formatted; } + + double get ap { + return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5); + } } extension DoubleExt on double { diff --git a/lib/common/print.dart b/lib/common/print.dart index 7be5625..fb9786b 100644 --- a/lib/common/print.dart +++ b/lib/common/print.dart @@ -1,4 +1,3 @@ -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'; @@ -20,10 +19,7 @@ class CommonPrint { return; } globalState.appController.addLog( - Log( - logLevel: LogLevel.info, - payload: payload, - ), + Log.app(payload), ); } } diff --git a/lib/common/request.dart b/lib/common/request.dart index c0fad05..c06135c 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -130,7 +130,7 @@ class Request { if (response.statusCode != HttpStatus.ok) { return false; } - return (response.data as String) == helperTag; + return (response.data as String) == globalState.coreSHA256; } catch (_) { return false; } diff --git a/lib/common/system.dart b/lib/common/system.dart index e0eadf9..20f26da 100644 --- a/lib/common/system.dart +++ b/lib/common/system.dart @@ -55,18 +55,24 @@ class System { } Future authorizeCore() async { + if (Platform.isAndroid) { + return AuthorizeCode.none; + } final corePath = appPath.corePath.replaceAll(' ', '\\\\ '); final isAdmin = await checkIsAdmin(); if (isAdmin) { return AuthorizeCode.none; } + if (Platform.isWindows) { final result = await windows?.registerService(); if (result == true) { return AuthorizeCode.success; } return AuthorizeCode.error; - } else if (Platform.isMacOS) { + } + + if (Platform.isMacOS) { final shell = 'chown root:admin $corePath; chmod +sx $corePath'; final arguments = [ "-e", diff --git a/lib/common/theme.dart b/lib/common/theme.dart index 64fdbd1..97e3912 100644 --- a/lib/common/theme.dart +++ b/lib/common/theme.dart @@ -4,36 +4,43 @@ import 'package:flutter/material.dart'; class CommonTheme { final BuildContext context; final Map _colorMap; + final double textScaleFactor; - CommonTheme.of(this.context) : _colorMap = {}; + CommonTheme.of( + this.context, + this.textScaleFactor, + ) : _colorMap = {}; Color get darkenSecondaryContainer { - return _colorMap.getCacheValue( + return _colorMap.updateCacheValue( "darkenSecondaryContainer", - context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1), + () => context.colorScheme.secondaryContainer + .blendDarken(context, factor: 0.1), ); } Color get darkenSecondaryContainerLighter { - return _colorMap.getCacheValue( + return _colorMap.updateCacheValue( "darkenSecondaryContainerLighter", - context.colorScheme.secondaryContainer + () => context.colorScheme.secondaryContainer .blendDarken(context, factor: 0.1) .opacity60, ); } Color get darken2SecondaryContainer { - return _colorMap.getCacheValue( + return _colorMap.updateCacheValue( "darken2SecondaryContainer", - context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2), + () => context.colorScheme.secondaryContainer + .blendDarken(context, factor: 0.2), ); } Color get darken3PrimaryContainer { - return _colorMap.getCacheValue( + return _colorMap.updateCacheValue( "darken3PrimaryContainer", - context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3), + () => context.colorScheme.primaryContainer + .blendDarken(context, factor: 0.3), ); } } diff --git a/lib/common/tray.dart b/lib/common/tray.dart index f2e744c..8c80aa6 100644 --- a/lib/common/tray.dart +++ b/lib/common/tray.dart @@ -80,7 +80,7 @@ class Tray { ); } menuItems.add(MenuItem.separator()); - if (!Platform.isWindows) { + if (Platform.isMacOS) { for (final group in trayState.groups) { List subMenuItems = []; for (final proxy in group.all) { diff --git a/lib/controller.dart b/lib/controller.dart index a7b99c6..5f60b0a 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -260,9 +260,7 @@ class AppController { final patchConfig = _ref.read(patchClashConfigProvider); final appSetting = _ref.read(appSettingProvider); bool enableTun = patchConfig.tun.enable; - if (enableTun != lastTunEnable && - lastTunEnable == false && - !Platform.isAndroid) { + if (enableTun != lastTunEnable && lastTunEnable == false) { final code = await system.authorizeCore(); switch (code) { case AuthorizeCode.none: @@ -314,6 +312,10 @@ class AppController { handleChangeProfile() { _ref.read(delayDataSourceProvider.notifier).value = {}; applyProfile(); + _ref.read(logsProvider.notifier).value = FixedList(500); + _ref.read(requestsProvider.notifier).value = FixedList(500); + globalState.cacheHeightMap = {}; + globalState.cacheScrollPosition = {}; } updateBrightness(Brightness brightness) { @@ -334,23 +336,22 @@ class AppController { try { await updateProfile(profile); } catch (e) { - _ref.read(logsProvider.notifier).addLog( - Log( - logLevel: LogLevel.info, - payload: e.toString(), - ), - ); + commonPrint.log(e.toString()); } } } Future updateGroups() async { - _ref.read(groupsProvider.notifier).value = await retry( - task: () async { - return await clashCore.getProxiesGroups(); - }, - retryIf: (res) => res.isEmpty, - ); + try { + _ref.read(groupsProvider.notifier).value = await retry( + task: () async { + return await clashCore.getProxiesGroups(); + }, + retryIf: (res) => res.isEmpty, + ); + } catch (_) { + _ref.read(groupsProvider.notifier).value = []; + } } updateProfiles() async { @@ -362,10 +363,6 @@ class AppController { } } - updateSystemColorSchemes(ColorSchemes colorSchemes) { - _ref.read(appSchemesProvider.notifier).value = colorSchemes; - } - savePreferences() async { commonPrint.log("save preferences"); await preferences.saveConfig(globalState.config); @@ -401,15 +398,23 @@ class AppController { handleExit() async { try { await updateStatus(false); + await proxy?.stopProxy(); await clashCore.shutdown(); await clashService?.destroy(); - await proxy?.stopProxy(); await savePreferences(); } finally { system.exit(); } } + Future handleClear() async { + await preferences.clearPreferences(); + commonPrint.log("clear preferences"); + globalState.config = Config( + themeProps: defaultThemeProps, + ); + } + autoCheckUpdate() async { if (!_ref.read(appSettingProvider).autoCheckUpdate) return; final res = await request.checkForUpdate(); @@ -484,10 +489,10 @@ class AppController { Future _initCore() async { final isInit = await clashCore.isInit; if (!isInit) { + await clashCore.init(); await clashCore.setState( globalState.getCoreState(), ); - await clashCore.init(); } await applyProfile(); } @@ -937,30 +942,39 @@ class AppController { } _recovery(Config config, RecoveryOption recoveryOption) { + final recoveryStrategy = _ref.read(appSettingProvider.select( + (state) => state.recoveryStrategy, + )); final profiles = config.profiles; - for (final profile in profiles) { - _ref.read(profilesProvider.notifier).setProfile(profile); + if (recoveryStrategy == RecoveryStrategy.override) { + _ref.read(profilesProvider.notifier).value = profiles; + } else { + for (final profile in profiles) { + _ref.read(profilesProvider.notifier).setProfile( + profile, + ); + } } final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles; - if (onlyProfiles) { - final currentProfile = _ref.read(currentProfileProvider); - if (currentProfile != null) { - _ref.read(currentProfileIdProvider.notifier).value = profiles.first.id; - } - return; + if (!onlyProfiles) { + _ref.read(patchClashConfigProvider.notifier).value = + config.patchClashConfig; + _ref.read(appSettingProvider.notifier).value = config.appSetting; + _ref.read(currentProfileIdProvider.notifier).value = + config.currentProfileId; + _ref.read(appDAVSettingProvider.notifier).value = config.dav; + _ref.read(themeSettingProvider.notifier).value = config.themeProps; + _ref.read(windowSettingProvider.notifier).value = config.windowProps; + _ref.read(vpnSettingProvider.notifier).value = config.vpnProps; + _ref.read(proxiesStyleSettingProvider.notifier).value = + config.proxiesStyle; + _ref.read(overrideDnsProvider.notifier).value = config.overrideDns; + _ref.read(networkSettingProvider.notifier).value = config.networkProps; + _ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions; + } + final currentProfile = _ref.read(currentProfileProvider); + if (currentProfile == null) { + _ref.read(currentProfileIdProvider.notifier).value = profiles.first.id; } - _ref.read(patchClashConfigProvider.notifier).value = - config.patchClashConfig; - _ref.read(appSettingProvider.notifier).value = config.appSetting; - _ref.read(currentProfileIdProvider.notifier).value = - config.currentProfileId; - _ref.read(appDAVSettingProvider.notifier).value = config.dav; - _ref.read(themeSettingProvider.notifier).value = config.themeProps; - _ref.read(windowSettingProvider.notifier).value = config.windowProps; - _ref.read(vpnSettingProvider.notifier).value = config.vpnProps; - _ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle; - _ref.read(overrideDnsProvider.notifier).value = config.overrideDns; - _ref.read(networkSettingProvider.notifier).value = config.networkProps; - _ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions; } } diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index fe7b2f3..d6e33b6 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -91,7 +91,14 @@ enum Mode { rule, global, direct } enum ViewMode { mobile, laptop, desktop } -enum LogLevel { debug, info, warning, error, silent } +enum LogLevel { + debug, + info, + warning, + error, + silent, + app, +} enum TransportProtocol { udp, tcp } @@ -262,6 +269,7 @@ enum ActionMethod { getCountryCode, getMemory, getProfile, + crash, ///Android, setFdMap, @@ -285,6 +293,7 @@ enum WindowsHelperServiceStatus { enum DebounceTag { updateClashConfig, + updateStatus, updateGroups, addCheckIpNum, applyProfile, @@ -308,6 +317,12 @@ enum DashboardWidget { child: NetworkSpeed(), ), ), + outboundModeV2( + GridItem( + crossAxisCellCount: 8, + child: OutboundModeV2(), + ), + ), outboundMode( GridItem( crossAxisCellCount: 4, @@ -333,6 +348,15 @@ enum DashboardWidget { ), platforms: desktopPlatforms, ), + vpnButton( + GridItem( + crossAxisCellCount: 4, + child: VpnButton(), + ), + platforms: [ + SupportPlatform.Android, + ], + ), systemProxyButton( GridItem( crossAxisCellCount: 4, @@ -447,3 +471,14 @@ enum RuleTarget { DIRECT, REJECT, } + +enum RecoveryStrategy { + compatible, + override, +} + +enum CacheTag { + logs, + rules, + requests, +} diff --git a/lib/fragments/about.dart b/lib/fragments/about.dart index 65d8ae5..c0e9e24 100644 --- a/lib/fragments/about.dart +++ b/lib/fragments/about.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; @immutable class Contributor { @@ -43,6 +47,15 @@ class AboutFragment extends StatelessWidget { _checkUpdate(context); }, ), + ListItem( + title: Text(appLocalizations.contactMe), + onTap: () { + globalState.showMessage( + title: appLocalizations.contactMe, + message: TextSpan(text: "chen08209@gmail.com"), + ); + }, + ), ListItem( title: const Text("Telegram"), onTap: () { @@ -116,33 +129,43 @@ class AboutFragment extends StatelessWidget { title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Wrap( - spacing: 16, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: Image.asset( - 'assets/images/icon.png', - width: 64, - height: 64, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Consumer(builder: (_, ref, ___) { + return _DeveloperModeDetector( + child: Wrap( + spacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - Text( - appName, - style: Theme.of(context).textTheme.headlineSmall, + Padding( + padding: const EdgeInsets.all(12), + child: Image.asset( + 'assets/images/icon.png', + width: 64, + height: 64, + ), ), - Text( - globalState.packageInfo.version, - style: Theme.of(context).textTheme.labelLarge, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appName, + style: Theme.of(context).textTheme.headlineSmall, + ), + Text( + globalState.packageInfo.version, + style: Theme.of(context).textTheme.labelLarge, + ) + ], ) ], - ) - ], - ), + ), + onEnterDeveloperMode: () { + ref.read(appSettingProvider.notifier).updateState( + (state) => state.copyWith(developerMode: true), + ); + context.showNotifier(appLocalizations.developerModeEnableTip); + }, + ); + }), const SizedBox( height: 24, ), @@ -209,3 +232,52 @@ class Avatar extends StatelessWidget { ); } } + +class _DeveloperModeDetector extends StatefulWidget { + final Widget child; + final VoidCallback onEnterDeveloperMode; + + const _DeveloperModeDetector({ + required this.child, + required this.onEnterDeveloperMode, + }); + + @override + State<_DeveloperModeDetector> createState() => _DeveloperModeDetectorState(); +} + +class _DeveloperModeDetectorState extends State<_DeveloperModeDetector> { + int _counter = 0; + Timer? _timer; + + void _handleTap() { + _counter++; + if (_counter >= 5) { + widget.onEnterDeveloperMode(); + _resetCounter(); + } else { + _timer?.cancel(); + _timer = Timer(Duration(seconds: 1), _resetCounter); + } + } + + void _resetCounter() { + _counter = 0; + _timer?.cancel(); + _timer = null; + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _handleTap, + child: widget.child, + ); + } +} diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index b62bd28..565e1e6 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -113,15 +113,11 @@ class _AccessFragmentState extends ConsumerState { } _intelligentSelected() async { - final appState = globalState.appState; - final config = globalState.config; - final accessControl = config.vpnProps.accessControl; - final packageNames = appState.packages - .where( - (item) => - accessControl.isFilterSystemApp ? item.isSystem == false : true, - ) - .map((item) => item.packageName); + final packageNames = ref.read( + packageListSelectorStateProvider.select( + (state) => state.list.map((item) => item.packageName), + ), + ); final commonScaffoldState = context.commonScaffoldState; if (commonScaffoldState?.mounted != true) return; final selectedPackageNames = @@ -194,7 +190,7 @@ class _AccessFragmentState extends ConsumerState { final state = ref.watch(packageListSelectorStateProvider); final accessControl = state.accessControl; final accessControlMode = accessControl.mode; - final packages = state.getList( + final packages = state.getSortList( accessControlMode == AccessControlMode.acceptSelected ? acceptList : rejectList, @@ -482,14 +478,20 @@ class AccessControlSearchDelegate extends SearchDelegate { final lowQuery = query.toLowerCase(); return Consumer( builder: (context, ref, __) { - final state = ref.watch(packageListSelectorStateProvider); - final accessControl = state.accessControl; - final accessControlMode = accessControl.mode; - final packages = state.getList( - accessControlMode == AccessControlMode.acceptSelected - ? acceptList - : rejectList, + final vm3 = ref.watch( + packageListSelectorStateProvider.select( + (state) => VM3( + a: state.getSortList( + state.accessControl.mode == AccessControlMode.acceptSelected + ? acceptList + : rejectList, + ), + b: state.accessControl.enable, + c: state.accessControl.currentList, + ), + ), ); + final packages = vm3.a; final queryPackages = packages .where( (package) => @@ -497,8 +499,8 @@ class AccessControlSearchDelegate extends SearchDelegate { package.packageName.contains(lowQuery), ) .toList(); - final isAccessControl = state.accessControl.enable; - final currentList = accessControl.currentList; + final isAccessControl = vm3.b; + final currentList = vm3.c; final packageNameList = packages.map((e) => e.packageName).toList(); final valueList = currentList.intersection(packageNameList); return DisabledMask( @@ -579,13 +581,6 @@ class _AccessControlPanelState extends ConsumerState { }; } - String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) { - return switch (isFilterSystemApp) { - true => appLocalizations.onlyOtherApps, - false => appLocalizations.allApps, - }; - } - List _buildModeSetting() { return generateSection( title: appLocalizations.mode, @@ -673,25 +668,39 @@ class _AccessControlPanelState extends ConsumerState { scrollDirection: Axis.horizontal, child: Consumer( builder: (_, ref, __) { - final isFilterSystemApp = ref.watch( - vpnSettingProvider - .select((state) => state.accessControl.isFilterSystemApp), + final vm2 = ref.watch( + vpnSettingProvider.select( + (state) => VM2( + a: state.accessControl.isFilterSystemApp, + b: state.accessControl.isFilterNonInternetApp, + ), + ), ); return Wrap( spacing: 16, children: [ - for (final item in [false, true]) - SettingTextCard( - _getTextWithIsFilterSystemApp(item), - isSelected: isFilterSystemApp == item, - onPressed: () { - ref.read(vpnSettingProvider.notifier).updateState( - (state) => state.copyWith.accessControl( - isFilterSystemApp: item, - ), - ); - }, - ) + SettingTextCard( + appLocalizations.systemApp, + isSelected: vm2.a == false, + onPressed: () { + ref.read(vpnSettingProvider.notifier).updateState( + (state) => state.copyWith.accessControl( + isFilterSystemApp: !vm2.a, + ), + ); + }, + ), + SettingTextCard( + appLocalizations.noNetworkApp, + isSelected: vm2.b == false, + onPressed: () { + ref.read(vpnSettingProvider.notifier).updateState( + (state) => state.copyWith.accessControl( + isFilterNonInternetApp: !vm2.b, + ), + ); + }, + ) ], ); }, diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index 2d963c0..dc1e594 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -8,10 +8,12 @@ import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/dialog.dart'; import 'package:fl_clash/widgets/fade_box.dart'; +import 'package:fl_clash/widgets/input.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; class BackupAndRecovery extends ConsumerWidget { const BackupAndRecovery({super.key}); @@ -134,6 +136,30 @@ class BackupAndRecovery extends ConsumerWidget { ); } + _handleUpdateRecoveryStrategy(WidgetRef ref) async { + final recoveryStrategy = ref.read(appSettingProvider.select( + (state) => state.recoveryStrategy, + )); + final res = await globalState.showCommonDialog( + child: OptionsDialog( + title: appLocalizations.recoveryStrategy, + options: RecoveryStrategy.values, + textBuilder: (mode) => Intl.message( + "recoveryStrategy_${mode.name}", + ), + value: recoveryStrategy, + ), + ); + if (res == null) { + return; + } + ref.read(appSettingProvider.notifier).updateState( + (state) => state.copyWith( + recoveryStrategy: res, + ), + ); + } + @override Widget build(BuildContext context, ref) { final dav = ref.watch(appDAVSettingProvider); @@ -256,6 +282,26 @@ class BackupAndRecovery extends ConsumerWidget { title: Text(appLocalizations.recovery), subtitle: Text(appLocalizations.localRecoveryDesc), ), + ListHeader(title: appLocalizations.options), + Consumer(builder: (_, ref, __) { + final recoveryStrategy = ref.watch(appSettingProvider.select( + (state) => state.recoveryStrategy, + )); + return ListItem( + onTap: () { + _handleUpdateRecoveryStrategy(ref); + }, + title: Text(appLocalizations.recoveryStrategy), + trailing: FilledButton( + onPressed: () { + _handleUpdateRecoveryStrategy(ref); + }, + child: Text( + Intl.message("recoveryStrategy_${recoveryStrategy.name}"), + ), + ), + ); + }), ], ); } diff --git a/lib/fragments/config/network.dart b/lib/fragments/config/network.dart index 1826ad9..10f6a82 100644 --- a/lib/fragments/config/network.dart +++ b/lib/fragments/config/network.dart @@ -301,8 +301,11 @@ class RouteAddressItem extends ConsumerWidget { title: appLocalizations.routeAddress, widget: Consumer( builder: (_, ref, __) { - final routeAddress = ref.watch(patchClashConfigProvider - .select((state) => state.tun.routeAddress)); + final routeAddress = ref.watch( + patchClashConfigProvider.select( + (state) => state.tun.routeAddress, + ), + ); return ListInputPage( title: appLocalizations.routeAddress, items: routeAddress, @@ -371,7 +374,9 @@ class NetworkListView extends ConsumerWidget { return; } ref.read(vpnSettingProvider.notifier).updateState( - (state) => defaultVpnProps, + (state) => defaultVpnProps.copyWith( + accessControl: state.accessControl, + ), ); ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith( diff --git a/lib/fragments/connection/requests.dart b/lib/fragments/connection/requests.dart index ba99fe2..e09aa40 100644 --- a/lib/fragments/connection/requests.dart +++ b/lib/fragments/connection/requests.dart @@ -20,12 +20,13 @@ class RequestsFragment extends ConsumerStatefulWidget { class _RequestsFragmentState extends ConsumerState with PageMixin { - final GlobalKey _key = GlobalKey(); - final _requestsStateNotifier = - ValueNotifier(const ConnectionsState()); + final _requestsStateNotifier = ValueNotifier( + const ConnectionsState(loading: true), + ); List _requests = []; - final _cacheKey = ValueKey("requests_list"); + final _tag = CacheTag.requests; late ScrollController _scrollController; + bool _isLoad = false; double _currentMaxWidth = 0; @@ -45,12 +46,13 @@ class _RequestsFragmentState extends ConsumerState @override void initState() { super.initState(); - final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1; + final preOffset = globalState.cacheScrollPosition[_tag] ?? -1; _scrollController = ScrollController( initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite, ); + _requests = globalState.appState.requests.list; _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( - connections: globalState.appState.requests.list, + connections: _requests, ); ref.listenManual( isCurrentPageProvider( @@ -73,7 +75,6 @@ class _RequestsFragmentState extends ConsumerState updateRequestsThrottler(); } }, - fireImmediately: true, ); } @@ -98,14 +99,7 @@ class _RequestsFragmentState extends ConsumerState final lines = (chainSize.height / baseHeight).round(); final computerHeight = size.height + chainSize.height + 24 + 24 * (lines - 1); - return computerHeight; - } - - _handleTryClearCache(double maxWidth) { - if (_currentMaxWidth != maxWidth) { - _currentMaxWidth = maxWidth; - _key.currentState?.clearCache(); - } + return computerHeight + 8 + 32 + globalState.measure.bodyMediumHeight; } @override @@ -133,6 +127,42 @@ class _RequestsFragmentState extends ConsumerState }, duration: commonDuration); } + _preLoad() { + if (_isLoad == true) { + return; + } + _isLoad = true; + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) { + return; + } + final isMobileView = ref.read(isMobileViewProvider); + if (isMobileView) { + await Future.delayed(Duration(milliseconds: 300)); + } + final parts = _requests.batch(10); + globalState.cacheHeightMap[_tag] ??= FixedMap( + _requests.length, + ); + for (int i = 0; i < parts.length; i++) { + final part = parts[i]; + await Future( + () { + for (final request in part) { + globalState.cacheHeightMap[_tag]?.updateCacheValue( + request.id, + () => _calcCacheHeight(request), + ); + } + }, + ); + } + _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( + loading: false, + ); + }); + } + @override Widget build(BuildContext context) { return LayoutBuilder( @@ -146,75 +176,86 @@ class _RequestsFragmentState extends ConsumerState Platform.isAndroid, ), ); - _handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); + _currentMaxWidth = constraints.maxWidth - 40 - (value ? 60 : 0); return child!; }, - child: ValueListenableBuilder( - valueListenable: _requestsStateNotifier, - builder: (_, state, __) { - final connections = state.list; - if (connections.isEmpty) { - return NullStatus( - label: appLocalizations.nullRequestsDesc, + child: TextScaleNotification( + child: ValueListenableBuilder( + valueListenable: _requestsStateNotifier, + builder: (_, state, __) { + _preLoad(); + final connections = state.list; + if (connections.isEmpty) { + return NullStatus( + label: appLocalizations.nullRequestsDesc, + ); + } + final items = connections + .map( + (connection) => ConnectionItem( + key: Key(connection.id), + connection: connection, + onClickKeyword: (value) { + context.commonScaffoldState?.addKeyword(value); + }, + ), + ) + .separated( + const Divider( + height: 0, + ), + ) + .toList(); + final content = connections.isEmpty + ? NullStatus( + label: appLocalizations.nullRequestsDesc, + ) + : Align( + alignment: Alignment.topCenter, + child: ScrollToEndBox( + controller: _scrollController, + tag: _tag, + dataSource: connections, + child: CommonScrollBar( + controller: _scrollController, + child: CacheItemExtentListView( + tag: _tag, + reverse: true, + shrinkWrap: true, + physics: NextClampingScrollPhysics(), + controller: _scrollController, + itemExtentBuilder: (index) { + if (index.isOdd) { + return 0; + } + return _calcCacheHeight( + connections[index ~/ 2]); + }, + itemBuilder: (_, index) { + return items[index]; + }, + itemCount: items.length, + keyBuilder: (int index) { + if (index.isOdd) { + return "divider"; + } + return connections[index ~/ 2].id; + }, + ), + ), + ), + ); + return FadeBox( + child: state.loading + ? Center( + child: CircularProgressIndicator(), + ) + : content, ); - } - final items = connections - .map( - (connection) => ConnectionItem( - key: Key(connection.id), - connection: connection, - onClickKeyword: (value) { - context.commonScaffoldState?.addKeyword(value); - }, - ), - ) - .separated( - const Divider( - height: 0, - ), - ) - .toList(); - return Align( - alignment: Alignment.topCenter, - child: ScrollToEndBox( - controller: _scrollController, - cacheKey: _cacheKey, - dataSource: connections, - child: CommonScrollBar( - controller: _scrollController, - child: CacheItemExtentListView( - key: _key, - reverse: true, - shrinkWrap: true, - physics: NextClampingScrollPhysics(), - controller: _scrollController, - itemExtentBuilder: (index) { - final widget = items[index]; - if (widget.runtimeType == Divider) { - return 0; - } - final measure = globalState.measure; - final bodyMediumHeight = measure.bodyMediumHeight; - final connection = connections[(index / 2).floor()]; - final height = _calcCacheHeight(connection); - return height + bodyMediumHeight + 32; - }, - itemBuilder: (_, index) { - return items[index]; - }, - itemCount: items.length, - keyBuilder: (int index) { - final widget = items[index]; - if (widget.runtimeType == Divider) { - return "divider"; - } - final connection = connections[(index / 2).floor()]; - return connection.id; - }, - ), - ), - ), - ); + }, + ), + onNotification: (_) { + globalState.cacheHeightMap[_tag]?.clear(); }, ), ); diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index bdcf91c..862e1ff 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -93,7 +93,7 @@ class _DashboardFragmentState extends ConsumerState @override Widget build(BuildContext context) { final dashboardState = ref.watch(dashboardStateProvider); - final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8); + final columns = max(4 * ((dashboardState.viewWidth / 320).ceil()), 8); return Align( alignment: Alignment.topCenter, child: SingleChildScrollView( @@ -103,8 +103,8 @@ class _DashboardFragmentState extends ConsumerState child: SuperGrid( key: key, crossAxisCount: columns, - crossAxisSpacing: 16, - mainAxisSpacing: 16, + crossAxisSpacing: 16.ap, + mainAxisSpacing: 16.ap, children: [ ...dashboardState.dashboardWidgets .where( diff --git a/lib/fragments/dashboard/widgets/memory_info.dart b/lib/fragments/dashboard/widgets/memory_info.dart index 3e0a90a..65dd98c 100644 --- a/lib/fragments/dashboard/widgets/memory_info.dart +++ b/lib/fragments/dashboard/widgets/memory_info.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/common.dart'; +import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; @@ -57,37 +58,43 @@ class _MemoryInfoState extends State { onPressed: () { clashCore.requestGc(); }, - child: Column( - children: [ - ValueListenableBuilder( - valueListenable: _memoryInfoStateNotifier, - builder: (_, trafficValue, __) { - return Padding( - padding: baseInfoEdgeInsets.copyWith( - bottom: 0, - top: 12, - ), - child: Row( - children: [ - Text( - trafficValue.showValue, - style: - context.textTheme.bodyMedium?.toLight.adjustSize(1), - ), - SizedBox( - width: 8, - ), - Text( - trafficValue.showUnit, - style: - context.textTheme.bodyMedium?.toLight.adjustSize(1), - ) - ], - ), - ); - }, - ), - ], + child: Container( + padding: baseInfoEdgeInsets.copyWith( + top: 0, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: globalState.measure.bodyMediumHeight + 2, + child: ValueListenableBuilder( + valueListenable: _memoryInfoStateNotifier, + builder: (_, trafficValue, __) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + trafficValue.showValue, + style: context.textTheme.bodyMedium?.toLight + .adjustSize(1), + ), + SizedBox( + width: 8, + ), + Text( + trafficValue.showUnit, + style: context.textTheme.bodyMedium?.toLight + .adjustSize(1), + ) + ], + ); + }, + ), + ) + ], + ), ), ), ); diff --git a/lib/fragments/dashboard/widgets/network_detection.dart b/lib/fragments/dashboard/widgets/network_detection.dart index 37fbd52..248f16c 100644 --- a/lib/fragments/dashboard/widgets/network_detection.dart +++ b/lib/fragments/dashboard/widgets/network_detection.dart @@ -206,7 +206,7 @@ class _NetworkDetectionState extends ConsumerState { ); }, icon: Icon( - size: 16, + size: 16.ap, Icons.info_outline, color: context.colorScheme.onSurfaceVariant, ), diff --git a/lib/fragments/dashboard/widgets/outbound_mode.dart b/lib/fragments/dashboard/widgets/outbound_mode.dart index 4c177b5..b989a1c 100644 --- a/lib/fragments/dashboard/widgets/outbound_mode.dart +++ b/lib/fragments/dashboard/widgets/outbound_mode.dart @@ -17,58 +17,146 @@ class OutboundMode extends StatelessWidget { height: height, child: Consumer( builder: (_, ref, __) { - final mode = - ref.watch(patchClashConfigProvider.select((state) => state.mode)); - return CommonCard( - onPressed: () {}, - info: Info( - label: appLocalizations.outboundMode, - iconData: Icons.call_split_sharp, - ), - child: Padding( - padding: const EdgeInsets.only( - top: 12, - bottom: 16, - ), - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - for (final item in Mode.values) - Flexible( - child: ListItem.radio( - dense: true, - horizontalTitleGap: 4, - padding: const EdgeInsets.only( - left: 12, - right: 16, - ), - delegate: RadioDelegate( - value: item, - groupValue: mode, - onChanged: (value) async { - if (value == null) { - return; - } - globalState.appController.changeMode(value); - }, - ), - title: Text( - Intl.message(item.name), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.toSoftBold, - ), - ), - ), - ], - ), + final mode = ref.watch( + patchClashConfigProvider.select( + (state) => state.mode, ), ); + return Theme( + data: Theme.of(context).copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent), + child: CommonCard( + onPressed: () {}, + info: Info( + label: appLocalizations.outboundMode, + iconData: Icons.call_split_sharp, + ), + child: Padding( + padding: const EdgeInsets.only( + top: 12, + bottom: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final item in Mode.values) + Flexible( + fit: FlexFit.tight, + child: ListItem.radio( + dense: true, + horizontalTitleGap: 4, + padding: EdgeInsets.only( + left: 12.ap, + right: 16.ap, + ), + delegate: RadioDelegate( + value: item, + groupValue: mode, + onChanged: (value) async { + if (value == null) { + return; + } + globalState.appController.changeMode(value); + }, + ), + title: Text( + Intl.message(item.name), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.toSoftBold, + ), + ), + ), + ], + ), + ), + )); }, ), ); } } + +class OutboundModeV2 extends StatelessWidget { + const OutboundModeV2({super.key}); + + Color _getTextColor(BuildContext context, Mode mode) { + return switch (mode) { + Mode.rule => context.colorScheme.onSecondaryContainer, + Mode.global => context.colorScheme.onPrimaryContainer, + Mode.direct => context.colorScheme.onTertiaryContainer, + }; + } + + @override + Widget build(BuildContext context) { + final height = getWidgetHeight(0.72); + return SizedBox( + height: height, + child: CommonCard( + padding: EdgeInsets.zero, + child: Consumer( + builder: (_, ref, __) { + final mode = ref.watch( + patchClashConfigProvider.select( + (state) => state.mode, + ), + ); + final thumbColor = switch (mode) { + Mode.rule => context.colorScheme.secondaryContainer, + Mode.global => globalState.theme.darken3PrimaryContainer, + Mode.direct => context.colorScheme.tertiaryContainer, + }; + return Container( + constraints: BoxConstraints.expand(), + child: CommonTabBar( + children: Map.fromEntries( + Mode.values.map( + (item) => MapEntry( + item, + Container( + clipBehavior: Clip.antiAlias, + alignment: Alignment.center, + decoration: BoxDecoration(), + height: height - 16, + child: Text( + Intl.message(item.name), + style: Theme.of(context) + .textTheme + .titleSmall + ?.adjustSize(1) + .copyWith( + color: item == mode + ? _getTextColor( + context, + item, + ) + : null, + ), + ), + ), + ), + ), + ), + padding: EdgeInsets.symmetric(horizontal: 8), + groupValue: mode, + onValueChanged: (value) { + if (value == null) { + return; + } + globalState.appController.changeMode(value); + }, + thumbColor: thumbColor, + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/quick_options.dart b/lib/fragments/dashboard/widgets/quick_options.dart index 22e4de5..604cc52 100644 --- a/lib/fragments/dashboard/widgets/quick_options.dart +++ b/lib/fragments/dashboard/widgets/quick_options.dart @@ -165,3 +165,87 @@ class SystemProxyButton extends StatelessWidget { ); } } + +class VpnButton extends StatelessWidget { + const VpnButton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: getWidgetHeight(1), + child: CommonCard( + onPressed: () { + showSheet( + context: context, + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: generateListView( + generateSection( + items: [ + const VPNItem(), + const VpnSystemProxyItem(), + const TunStackItem(), + ], + ), + ), + title: "VPN", + ); + }, + ); + }, + info: Info( + label: "VPN", + iconData: Icons.stacked_line_chart, + ), + child: Container( + padding: baseInfoEdgeInsets.copyWith( + top: 4, + bottom: 8, + right: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: TooltipText( + text: Text( + appLocalizations.options, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleSmall + ?.adjustSize(-2) + .toLight, + ), + ), + ), + Consumer( + builder: (_, ref, __) { + final enable = ref.watch( + vpnSettingProvider.select( + (state) => state.enable, + ), + ); + return Switch( + value: enable, + onChanged: (value) { + ref.read(vpnSettingProvider.notifier).updateState( + (state) => state.copyWith( + enable: value, + ), + ); + }, + ); + }, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/start_button.dart b/lib/fragments/dashboard/widgets/start_button.dart index 350b36c..66c1491 100644 --- a/lib/fragments/dashboard/widgets/start_button.dart +++ b/lib/fragments/dashboard/widgets/start_button.dart @@ -1,5 +1,5 @@ import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -35,11 +35,15 @@ class _StartButtonState extends State } handleSwitchStart() { - if (isStart == globalState.appState.isStart) { - isStart = !isStart; - updateController(); - globalState.appController.updateStatus(isStart); - } + isStart = !isStart; + updateController(); + debouncer.call( + DebounceTag.updateStatus, + () { + globalState.appController.updateStatus(isStart); + }, + duration: moreDuration, + ); } updateController() { @@ -126,9 +130,11 @@ class _StartButtonState extends State final text = utils.getTimeText(runTime); return Text( text, - style: Theme.of(context).textTheme.titleMedium?.toSoftBold.copyWith( - color: context.colorScheme.onPrimaryContainer - ), + style: Theme.of(context) + .textTheme + .titleMedium + ?.toSoftBold + .copyWith(color: context.colorScheme.onPrimaryContainer), ); }, ), diff --git a/lib/fragments/developer.dart b/lib/fragments/developer.dart new file mode 100644 index 0000000..bf676c7 --- /dev/null +++ b/lib/fragments/developer.dart @@ -0,0 +1,120 @@ +import 'package:fl_clash/clash/core.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/common.dart'; +import 'package:fl_clash/providers/config.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../providers/app.dart'; + +class DeveloperView extends ConsumerWidget { + const DeveloperView({super.key}); + + Widget _getDeveloperList(BuildContext context, WidgetRef ref) { + return generateSectionV2( + title: appLocalizations.options, + items: [ + ListItem( + title: Text(appLocalizations.messageTest), + onTap: () { + context.showNotifier( + appLocalizations.messageTestTip, + ); + }, + ), + ListItem( + title: Text(appLocalizations.logsTest), + onTap: () { + for (int i = 0; i < 1000; i++) { + ref.read(requestsProvider.notifier).addRequest(Connection( + id: utils.id, + start: DateTime.now(), + metadata: Metadata( + uid: i * i, + network: utils.generateRandomString( + maxLength: 1000, + minLength: 20, + ), + sourceIP: '', + sourcePort: '', + destinationIP: '', + destinationPort: '', + host: '', + process: '', + remoteDestination: "", + ), + chains: ["chains"], + )); + globalState.appController.addLog( + Log.app( + utils.generateRandomString( + maxLength: 200, + minLength: 20, + ), + ), + ); + } + }, + ), + ListItem( + title: Text(appLocalizations.crashTest), + onTap: () { + clashCore.clashInterface.crash(); + }, + ), + ListItem( + title: Text(appLocalizations.clearData), + onTap: () async { + await globalState.appController.handleClear(); + }, + ) + ], + ); + } + + @override + Widget build(BuildContext context, ref) { + final enable = ref.watch( + appSettingProvider.select( + (state) => state.developerMode, + ), + ); + return SingleChildScrollView( + padding: baseInfoEdgeInsets, + child: Column( + children: [ + CommonCard( + type: CommonCardType.filled, + radius: 18, + child: ListItem.switchItem( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 4, + bottom: 4, + ), + title: Text(appLocalizations.developerMode), + delegate: SwitchDelegate( + value: enable, + onChanged: (value) { + ref.read(appSettingProvider.notifier).updateState( + (state) => state.copyWith( + developerMode: value, + ), + ); + }, + ), + ), + ), + SizedBox( + height: 16, + ), + _getDeveloperList(context, ref) + ], + ), + ); + } +} diff --git a/lib/fragments/fragments.dart b/lib/fragments/fragments.dart index c3daf82..5fa5c50 100644 --- a/lib/fragments/fragments.dart +++ b/lib/fragments/fragments.dart @@ -11,3 +11,4 @@ export 'backup_and_recovery.dart'; export 'resources.dart'; export 'connection/requests.dart'; export 'connection/connections.dart'; +export 'developer.dart'; diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index af09186..6f75302 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/providers/providers.dart'; @@ -16,36 +18,27 @@ class LogsFragment extends ConsumerStatefulWidget { } class _LogsFragmentState extends ConsumerState with PageMixin { - final _logsStateNotifier = ValueNotifier(LogsState()); - final _cacheKey = ValueKey("logs_list"); + final _logsStateNotifier = ValueNotifier( + LogsState(loading: true), + ); late ScrollController _scrollController; + double _currentMaxWidth = 0; - final GlobalKey _key = GlobalKey(); + final _tag = CacheTag.rules; + bool _isLoad = false; List _logs = []; @override void initState() { super.initState(); - final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1; + final position = globalState.cacheScrollPosition[_tag] ?? -1; _scrollController = ScrollController( - initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite, + initialScrollOffset: position > 0 ? position : double.maxFinite, ); + _logs = globalState.appState.logs.list; _logsStateNotifier.value = _logsStateNotifier.value.copyWith( - logs: globalState.appState.logs.list, - ); - ref.listenManual( - logsProvider.select((state) => state.list), - (prev, next) { - if (prev != next) { - final isEquality = logListEquality.equals(prev, next); - if (!isEquality) { - _logs = next; - updateLogsThrottler(); - } - } - }, - fireImmediately: true, + logs: _logs, ); ref.listenManual( isCurrentPageProvider( @@ -60,6 +53,18 @@ class _LogsFragmentState extends ConsumerState with PageMixin { }, fireImmediately: true, ); + ref.listenManual( + logsProvider.select((state) => state.list), + (prev, next) { + if (prev != next) { + final isEquality = logListEquality.equals(prev, next); + if (!isEquality) { + _logs = next; + updateLogsThrottler(); + } + } + }, + ); } @override @@ -94,13 +99,6 @@ class _LogsFragmentState extends ConsumerState with PageMixin { super.dispose(); } - _handleTryClearCache(double maxWidth) { - if (_currentMaxWidth != maxWidth) { - _currentMaxWidth = maxWidth; - _key.currentState?.clearCache(); - } - } - _handleExport() async { final commonScaffoldState = context.commonScaffoldState; final res = await commonScaffoldState?.loadingRun( @@ -123,13 +121,13 @@ class _LogsFragmentState extends ConsumerState with PageMixin { final height = globalState.measure .computeTextSize( Text( - log.payload ?? "", - style: globalState.appController.context.textTheme.bodyLarge, + log.payload, + style: context.textTheme.bodyLarge, ), maxWidth: _currentMaxWidth, ) .height; - return height + bodySmallHeight + 8 + bodyMediumHeight + 40; + return height + bodySmallHeight + 8 + bodyMediumHeight + 40 + 8; } updateLogsThrottler() { @@ -142,82 +140,123 @@ class _LogsFragmentState extends ConsumerState with PageMixin { return; } WidgetsBinding.instance.addPostFrameCallback((_) { - _logsStateNotifier.value = _logsStateNotifier.value.copyWith( - logs: _logs, - ); + if (mounted) { + _logsStateNotifier.value = _logsStateNotifier.value.copyWith( + logs: _logs, + ); + } }); }, duration: commonDuration); } + _preLoad() { + if (_isLoad == true) { + return; + } + _isLoad = true; + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) { + return; + } + final isMobileView = ref.read(isMobileViewProvider); + if (isMobileView) { + await Future.delayed(Duration(milliseconds: 300)); + } + final parts = _logs.batch(10); + globalState.cacheHeightMap[_tag] ??= FixedMap( + _logs.length, + ); + for (int i = 0; i < parts.length; i++) { + final part = parts[i]; + await Future( + () { + for (final log in part) { + globalState.cacheHeightMap[_tag]?.updateCacheValue( + log.payload, + () => _getItemHeight(log), + ); + } + }, + ); + } + _logsStateNotifier.value = _logsStateNotifier.value.copyWith( + loading: false, + ); + }); + } + @override Widget build(BuildContext context) { return LayoutBuilder( builder: (_, constraints) { - _handleTryClearCache(constraints.maxWidth - 40); - return Align( - alignment: Alignment.topCenter, - child: ValueListenableBuilder( - valueListenable: _logsStateNotifier, - builder: (_, state, __) { - final logs = state.list; - if (logs.isEmpty) { - return NullStatus( - label: appLocalizations.nullLogsDesc, - ); - } - final items = logs - .map( - (log) => LogItem( - key: Key(log.dateTime.toString()), - log: log, - onClick: (value) { - context.commonScaffoldState?.addKeyword(value); - }, - ), - ) - .separated( - const Divider( - height: 0, - ), - ) - .toList(); - return ScrollToEndBox( - controller: _scrollController, - cacheKey: _cacheKey, - dataSource: logs, - child: CommonScrollBar( - controller: _scrollController, - child: CacheItemExtentListView( - key: _key, - reverse: true, - shrinkWrap: true, - physics: NextClampingScrollPhysics(), - controller: _scrollController, - itemBuilder: (_, index) { - return items[index]; - }, - itemExtentBuilder: (index) { - final item = items[index]; - if (item.runtimeType == Divider) { - return 0; - } - final log = logs[(index / 2).floor()]; - return _getItemHeight(log); - }, - itemCount: items.length, - keyBuilder: (int index) { - final item = items[index]; - if (item.runtimeType == Divider) { - return "divider"; - } - final log = logs[(index / 2).floor()]; - return log.payload ?? ""; + _currentMaxWidth = constraints.maxWidth - 40; + return ValueListenableBuilder( + valueListenable: _logsStateNotifier, + builder: (_, state, __) { + _preLoad(); + final logs = state.list; + final items = logs + .map( + (log) => LogItem( + key: Key(log.dateTime), + log: log, + onClick: (value) { + context.commonScaffoldState?.addKeyword(value); }, ), - ), - ); - }, - ), + ) + .separated( + const Divider( + height: 0, + ), + ) + .toList(); + final content = logs.isEmpty + ? NullStatus( + label: appLocalizations.nullLogsDesc, + ) + : Align( + alignment: Alignment.topCenter, + child: CommonScrollBar( + controller: _scrollController, + child: ScrollToEndBox( + controller: _scrollController, + tag: _tag, + dataSource: logs, + child: CacheItemExtentListView( + tag: _tag, + reverse: true, + shrinkWrap: true, + physics: NextClampingScrollPhysics(), + controller: _scrollController, + itemBuilder: (_, index) { + return items[index]; + }, + itemExtentBuilder: (index) { + if (index.isOdd) { + return 0; + } + return _getItemHeight(logs[index ~/ 2]); + }, + itemCount: items.length, + keyBuilder: (int index) { + if (index.isOdd) { + return "divider"; + } + return logs[index ~/ 2].payload; + }, + ), + ), + ), + ); + return FadeBox( + child: state.loading + ? Center( + child: CircularProgressIndicator(), + ) + : content, + ); + }, ); }, ); @@ -242,14 +281,14 @@ class LogItem extends StatelessWidget { vertical: 4, ), title: SelectableText( - log.payload ?? '', + log.payload, style: context.textTheme.bodyLarge, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SelectableText( - "${log.dateTime}", + log.dateTime, style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.primary, ), diff --git a/lib/fragments/profiles/add_profile.dart b/lib/fragments/profiles/add_profile.dart index 0f7a02f..749870b 100644 --- a/lib/fragments/profiles/add_profile.dart +++ b/lib/fragments/profiles/add_profile.dart @@ -104,8 +104,13 @@ class _URLFormDialogState extends State { runSpacing: 16, children: [ TextField( - maxLines: 5, + keyboardType: TextInputType.url, minLines: 1, + maxLines: 5, + onSubmitted: (_) { + _handleAddProfileFormURL(); + }, + onEditingComplete: _handleAddProfileFormURL, controller: urlController, decoration: InputDecoration( border: const OutlineInputBorder(), diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 1cee6bb..44a3f38 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -214,6 +214,7 @@ class _EditProfileState extends State { final items = [ ListItem( title: TextFormField( + textInputAction: TextInputAction.next, controller: labelController, decoration: InputDecoration( border: const OutlineInputBorder(), @@ -230,6 +231,8 @@ class _EditProfileState extends State { if (widget.profile.type == ProfileType.url) ...[ ListItem( title: TextFormField( + textInputAction: TextInputAction.next, + keyboardType: TextInputType.url, controller: urlController, maxLines: 5, minLines: 1, @@ -258,6 +261,7 @@ class _EditProfileState extends State { if (autoUpdate) ListItem( title: TextFormField( + textInputAction: TextInputAction.next, controller: autoUpdateDurationController, decoration: InputDecoration( border: const OutlineInputBorder(), diff --git a/lib/fragments/profiles/override_profile.dart b/lib/fragments/profiles/override_profile.dart index f1d0afb..b6b4cbf 100644 --- a/lib/fragments/profiles/override_profile.dart +++ b/lib/fragments/profiles/override_profile.dart @@ -23,7 +23,6 @@ class OverrideProfile extends StatefulWidget { } class _OverrideProfileState extends State { - final GlobalKey _ruleListKey = GlobalKey(); final _controller = ScrollController(); double _currentMaxWidth = 0; @@ -86,13 +85,6 @@ class _OverrideProfileState extends State { ); } - _handleTryClearCache(double maxWidth) { - if (_currentMaxWidth != maxWidth) { - _currentMaxWidth = maxWidth; - _ruleListKey.currentState?.clearCache(); - } - } - _buildContent() { return Consumer( builder: (_, ref, child) { @@ -116,7 +108,7 @@ class _OverrideProfileState extends State { }, child: LayoutBuilder( builder: (_, constraints) { - _handleTryClearCache(constraints.maxWidth - 104); + _currentMaxWidth = constraints.maxWidth - 104; return CommonAutoHiddenScrollBar( controller: _controller, child: CustomScrollView( @@ -148,7 +140,6 @@ class _OverrideProfileState extends State { padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 0), sliver: RuleContent( maxWidth: _currentMaxWidth, - ruleListKey: _ruleListKey, ), ), SliverToBoxAdapter( @@ -228,7 +219,7 @@ class _OverrideProfileState extends State { message: TextSpan( text: appLocalizations.saveTip, ), - confirmText: appLocalizations.tip, + confirmText: appLocalizations.save, ); if (res != true) { return; @@ -449,12 +440,10 @@ class RuleTitle extends ConsumerWidget { } class RuleContent extends ConsumerWidget { - final Key ruleListKey; final double maxWidth; const RuleContent({ super.key, - required this.ruleListKey, required this.maxWidth, }); @@ -602,7 +591,7 @@ class RuleContent extends ConsumerWidget { ); } return CacheItemExtentSliverReorderableList( - key: ruleListKey, + tag: CacheTag.rules, itemBuilder: (context, index) { final rule = rules[index]; return GestureDetector( @@ -873,6 +862,8 @@ class _AddRuleDialogState extends State { builder: (filed) { return DropdownMenu( width: 200, + enableFilter: false, + enableSearch: false, controller: _subRuleController, label: Text(appLocalizations.subRule), menuHeight: 250, @@ -890,11 +881,11 @@ class _AddRuleDialogState extends State { builder: (filed) { return DropdownMenu( controller: _ruleTargetController, - initialSelection: filed.value, label: Text(appLocalizations.ruleTarget), width: 200, menuHeight: 250, - enableFilter: true, + enableFilter: false, + enableSearch: false, dropdownMenuEntries: _targetItems, errorText: filed.errorText, ); diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 2934899..c34055d 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -370,7 +370,7 @@ class ProfileItem extends StatelessWidget { ), ), title: Container( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart index e6bdea4..34ed1a0 100644 --- a/lib/fragments/proxies/common.dart +++ b/lib/fragments/proxies/common.dart @@ -6,7 +6,7 @@ import 'package:fl_clash/state.dart'; double get listHeaderHeight { final measure = globalState.measure; - return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight; + return 28 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight; } double getItemHeight(ProxyCardType proxyCardType) { diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index 9612a60..5cb82e6 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -414,7 +414,10 @@ class _ListHeaderState extends State return Consumer( builder: (_, ref, child) { final iconStyle = ref.watch( - proxiesStyleSettingProvider.select((state) => state.iconStyle)); + proxiesStyleSettingProvider.select( + (state) => state.iconStyle, + ), + ); final icon = ref.watch(proxiesStyleSettingProvider.select((state) { final iconMapEntryList = state.iconMap.entries.toList(); final index = iconMapEntryList.indexWhere((item) { @@ -430,30 +433,44 @@ class _ListHeaderState extends State return this.icon; })); return switch (iconStyle) { - ProxiesIconStyle.standard => Container( - height: 48, - width: 48, - margin: const EdgeInsets.only( - right: 16, - ), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: context.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(12), - ), - clipBehavior: Clip.antiAlias, - child: CommonTargetIcon( - src: icon, - size: 32, - ), + ProxiesIconStyle.standard => LayoutBuilder( + builder: (_, constraints) { + return Container( + margin: const EdgeInsets.only( + right: 16, + ), + child: AspectRatio( + aspectRatio: 1, + child: Container( + height: constraints.maxHeight, + width: constraints.maxWidth, + alignment: Alignment.center, + padding: EdgeInsets.all(6.ap), + decoration: BoxDecoration( + color: context.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(12), + ), + clipBehavior: Clip.antiAlias, + child: CommonTargetIcon( + src: icon, + size: constraints.maxHeight - 12.ap, + ), + ), + ), + ); + }, ), ProxiesIconStyle.icon => Container( margin: const EdgeInsets.only( right: 16, ), - child: CommonTargetIcon( - src: icon, - size: 42, + child: LayoutBuilder( + builder: (_, constraints) { + return CommonTargetIcon( + src: icon, + size: constraints.maxHeight - 8, + ); + }, ), ), ProxiesIconStyle.none => Container(), diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index d454726..0848ffb 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -78,7 +78,7 @@ class _ProxiesFragmentState extends ConsumerState return AdaptiveSheetScaffold( type: type, body: const ProxiesSetting(), - title: appLocalizations.proxiesSetting, + title: appLocalizations.settings, ); }, ); @@ -123,8 +123,13 @@ class _ProxiesFragmentState extends ConsumerState @override Widget build(BuildContext context) { - final proxiesType = - ref.watch(proxiesStyleSettingProvider.select((state) => state.type)); + final proxiesType = ref.watch( + proxiesStyleSettingProvider.select( + (state) => state.type, + ), + ); + + ref.watch(themeSettingProvider.select((state) => state.textScale)); return switch (proxiesType) { ProxiesType.tab => ProxiesTabFragment( key: _proxiesTabKey, diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index 22b7508..320b39e 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -147,22 +147,21 @@ class _GeoDataListItemState extends State { FutureBuilder( future: _getGeoFileLastModified(geoItem.fileName), builder: (_, snapshot) { + final height = globalState.measure.bodyMediumHeight; return SizedBox( - height: 24, - child: FadeThroughBox( - key: Key("fade_box_${geoItem.label}"), - child: snapshot.data == null - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : Text( - snapshot.data!.desc, + height: height, + child: snapshot.data == null + ? SizedBox( + width: height, + height: height, + child: CircularProgressIndicator( + strokeWidth: 2, ), - ), + ) + : Text( + snapshot.data!.desc, + style: context.textTheme.bodyMedium, + ), ); }, ), diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index cbf7a5f..842e7ad 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use + import 'dart:math'; import 'dart:ui' as ui; @@ -39,7 +41,20 @@ class ThemeFragment extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView(child: ThemeColorsBox()); + return SingleChildScrollView( + child: Column( + spacing: 24, + children: [ + _ThemeModeItem(), + _PrimaryColorItem(), + _PrueBlackItem(), + _TextScaleFactorItem(), + const SizedBox( + height: 64, + ), + ], + ), + ); } } @@ -57,42 +72,14 @@ class ItemCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( - top: 16, - ), - child: Wrap( - runSpacing: 16, - children: [ - InfoHeader( - info: info, - actions: actions, - ), - child, - ], - ), - ); - } -} - -class ThemeColorsBox extends ConsumerStatefulWidget { - const ThemeColorsBox({super.key}); - - @override - ConsumerState createState() => _ThemeColorsBoxState(); -} - -class _ThemeColorsBoxState extends ConsumerState { - @override - Widget build(BuildContext context) { - return Column( + return Wrap( + runSpacing: 16, children: [ - _ThemeModeItem(), - _PrimaryColorItem(), - _PrueBlackItem(), - const SizedBox( - height: 64, + InfoHeader( + info: info, + actions: actions, ), + child, ], ); } @@ -296,137 +283,153 @@ class _PrimaryColorItemState extends ConsumerState<_PrimaryColorItem> { @override Widget build(BuildContext context) { - final vm3 = ref.watch( + final vm4 = ref.watch( themeSettingProvider.select( - (state) => VM3( + (state) => VM4( a: state.primaryColor, b: state.primaryColors, c: state.schemeVariant, + d: state.primaryColor == defaultPrimaryColor && + intListEquality.equals(state.primaryColors, defaultPrimaryColors), ), ), ); - final primaryColor = vm3.a; - final primaryColors = [null, ...vm3.b]; - final schemeVariant = vm3.c; + final primaryColor = vm4.a; + final primaryColors = [null, ...vm4.b]; + final schemeVariant = vm4.c; + final isEquals = vm4.d; - return ItemCard( - info: Info( - label: appLocalizations.themeColor, - iconData: Icons.palette, - ), - actions: genActions( - [ - if (_removablePrimaryColor == null) - FilledButton( - style: ButtonStyle( - visualDensity: VisualDensity.compact, - ), - onPressed: _handleChangeSchemeVariant, - child: Text(Intl.message("${schemeVariant.name}Scheme")), - ), - _removablePrimaryColor != null - ? FilledButton( - style: ButtonStyle( - visualDensity: VisualDensity.compact, - ), - onPressed: () { - setState(() { - _removablePrimaryColor = null; - }); - }, - child: Text(appLocalizations.cancel), - ) - : IconButton.filledTonal( - iconSize: 20, - padding: EdgeInsets.all(4), - visualDensity: VisualDensity.compact, - onPressed: _handleReset, - icon: Icon(Icons.replay), - ) - ], - space: 8, - ), - child: Container( - margin: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, + return CommonPopScope( + onPop: () { + if (_removablePrimaryColor != null) { + setState(() { + _removablePrimaryColor = null; + }); + return false; + } + return true; + }, + child: ItemCard( + info: Info( + label: appLocalizations.themeColor, + iconData: Icons.palette, ), - child: LayoutBuilder( - builder: (_, constraints) { - final columns = _calcColumns(constraints.maxWidth); - final itemWidth = - (constraints.maxWidth - (columns - 1) * 16) / columns; - return Wrap( - spacing: 16, - runSpacing: 16, - children: [ - for (final color in primaryColors) - Container( - clipBehavior: Clip.none, - width: itemWidth, - height: itemWidth, - child: Stack( - alignment: Alignment.center, + actions: genActions( + [ + if (_removablePrimaryColor == null) + FilledButton( + style: ButtonStyle( + visualDensity: VisualDensity.compact, + ), + onPressed: _handleChangeSchemeVariant, + child: Text(Intl.message("${schemeVariant.name}Scheme")), + ), + if (_removablePrimaryColor != null) + FilledButton( + style: ButtonStyle( + visualDensity: VisualDensity.compact, + ), + onPressed: () { + setState(() { + _removablePrimaryColor = null; + }); + }, + child: Text(appLocalizations.cancel), + ), + if (_removablePrimaryColor == null && !isEquals) + IconButton.filledTonal( + iconSize: 20, + padding: EdgeInsets.all(4), + visualDensity: VisualDensity.compact, + onPressed: _handleReset, + icon: Icon(Icons.replay), + ) + ], + space: 8, + ), + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: LayoutBuilder( + builder: (_, constraints) { + final columns = _calcColumns(constraints.maxWidth); + final itemWidth = + (constraints.maxWidth - (columns - 1) * 16) / columns; + return Wrap( + spacing: 16, + runSpacing: 16, + children: [ + for (final color in primaryColors) + Container( clipBehavior: Clip.none, - children: [ - EffectGestureDetector( - child: ColorSchemeBox( - isSelected: color == primaryColor, - primaryColor: color != null ? Color(color) : null, - onPressed: () { - ref - .read(themeSettingProvider.notifier) - .updateState( - (state) => state.copyWith( - primaryColor: color, - ), - ); + width: itemWidth, + height: itemWidth, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + EffectGestureDetector( + child: ColorSchemeBox( + isSelected: color == primaryColor, + primaryColor: color != null ? Color(color) : null, + onPressed: () { + setState(() { + _removablePrimaryColor = null; + }); + ref + .read(themeSettingProvider.notifier) + .updateState( + (state) => state.copyWith( + primaryColor: color, + ), + ); + }, + ), + onLongPress: () { + setState(() { + _removablePrimaryColor = color; + }); }, ), - onLongPress: () { - setState(() { - _removablePrimaryColor = color; - }); - }, - ), - if (_removablePrimaryColor != null && - _removablePrimaryColor == color) - Container( - color: Colors.white.opacity0, - padding: EdgeInsets.all(8), - child: IconButton.filledTonal( - onPressed: _handleDel, - padding: EdgeInsets.all(12), - iconSize: 30, - icon: Icon( - color: context.colorScheme.primary, - Icons.delete, + if (_removablePrimaryColor != null && + _removablePrimaryColor == color) + Container( + color: Colors.white.opacity0, + padding: EdgeInsets.all(8), + child: IconButton.filledTonal( + onPressed: _handleDel, + padding: EdgeInsets.all(12), + iconSize: 30, + icon: Icon( + color: context.colorScheme.primary, + Icons.delete, + ), ), ), - ), - ], - ), - ), - if (_removablePrimaryColor == null) - Container( - width: itemWidth, - height: itemWidth, - padding: EdgeInsets.all( - 4, - ), - child: IconButton.filledTonal( - onPressed: _handleAdd, - iconSize: 32, - icon: Icon( - color: context.colorScheme.primary, - Icons.add, + ], ), ), - ) - ], - ); - }, + if (_removablePrimaryColor == null) + Container( + width: itemWidth, + height: itemWidth, + padding: EdgeInsets.all( + 4, + ), + child: IconButton.filledTonal( + onPressed: _handleAdd, + iconSize: 32, + icon: Icon( + color: context.colorScheme.primary, + Icons.add, + ), + ), + ) + ], + ); + }, + ), ), ), ); @@ -438,33 +441,118 @@ class _PrueBlackItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final prueBlack = - ref.watch(themeSettingProvider.select((state) => state.pureBlack)); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: ListItem.switchItem( - leading: Icon( - Icons.contrast, - ), - horizontalTitleGap: 12, - title: Text( - appLocalizations.pureBlackMode, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurfaceVariant, - ), - ), - delegate: SwitchDelegate( - value: prueBlack, - onChanged: (value) { - ref.read(themeSettingProvider.notifier).updateState( - (state) => state.copyWith( - pureBlack: value, - ), - ); - }, - ), + final prueBlack = ref.watch( + themeSettingProvider.select( + (state) => state.pureBlack, ), ); + return ListItem.switchItem( + leading: Icon( + Icons.contrast, + ), + horizontalTitleGap: 12, + title: Text( + appLocalizations.pureBlackMode, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + ), + delegate: SwitchDelegate( + value: prueBlack, + onChanged: (value) { + ref.read(themeSettingProvider.notifier).updateState( + (state) => state.copyWith( + pureBlack: value, + ), + ); + }, + ), + ); + } +} + +class _TextScaleFactorItem extends ConsumerWidget { + const _TextScaleFactorItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textScale = ref.watch( + themeSettingProvider.select( + (state) => state.textScale, + ), + ); + final String process = "${((textScale.scale * 100) as double).round()}%"; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 8), + child: ListItem.switchItem( + leading: Icon( + Icons.text_fields, + ), + horizontalTitleGap: 12, + title: Text( + appLocalizations.textScale, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + ), + delegate: SwitchDelegate( + value: textScale.enable, + onChanged: (value) { + ref.read(themeSettingProvider.notifier).updateState( + (state) => state.copyWith.textScale( + enable: value, + ), + ); + }, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + spacing: 32, + children: [ + Expanded( + child: DisabledMask( + status: !textScale.enable, + child: ActivateBox( + active: textScale.enable, + child: SliderTheme( + data: _SliderDefaultsM3(context), + child: Slider( + padding: EdgeInsets.zero, + min: minTextScale, + max: maxTextScale, + value: textScale.scale, + onChanged: (value) { + ref.read(themeSettingProvider.notifier).updateState( + (state) => state.copyWith.textScale( + scale: value, + ), + ); + }, + ), + ), + ), + ), + ), + Padding( + padding: EdgeInsets.only(right: 4), + child: Text( + process, + style: context.textTheme.titleMedium, + ), + ), + ], + ), + ), + ], + ); } } @@ -530,3 +618,112 @@ class _PaletteDialogState extends State<_PaletteDialog> { ); } } + +class _SliderDefaultsM3 extends SliderThemeData { + _SliderDefaultsM3(this.context) : super(trackHeight: 16.0); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + Color? get activeTrackColor => _colors.primary; + + @override + Color? get inactiveTrackColor => _colors.secondaryContainer; + + @override + Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54); + + @override + Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12); + + @override + Color? get disabledSecondaryActiveTrackColor => + _colors.onSurface.withOpacity(0.38); + + @override + Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0); + + @override + Color? get inactiveTickMarkColor => + _colors.onSecondaryContainer.withOpacity(1.0); + + @override + Color? get disabledActiveTickMarkColor => _colors.onInverseSurface; + + @override + Color? get disabledInactiveTickMarkColor => _colors.onSurface; + + @override + Color? get thumbColor => _colors.primary; + + @override + Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get overlayColor => + WidgetStateColor.resolveWith((Set states) { + if (states.contains(WidgetState.dragged)) { + return _colors.primary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withOpacity(0.1); + } + + return Colors.transparent; + }); + + @override + TextStyle? get valueIndicatorTextStyle => + Theme.of(context).textTheme.labelLarge!.copyWith( + color: _colors.onInverseSurface, + ); + + @override + Color? get valueIndicatorColor => _colors.inverseSurface; + + @override + SliderComponentShape? get valueIndicatorShape => + const RoundedRectSliderValueIndicatorShape(); + + @override + SliderComponentShape? get thumbShape => const HandleThumbShape(); + + @override + SliderTrackShape? get trackShape => const GappedSliderTrackShape(); + + @override + SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); + + @override + SliderTickMarkShape? get tickMarkShape => + const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2); + + @override + WidgetStateProperty? get thumbSize { + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return const Size(4.0, 44.0); + } + if (states.contains(WidgetState.hovered)) { + return const Size(4.0, 44.0); + } + if (states.contains(WidgetState.focused)) { + return const Size(2.0, 44.0); + } + if (states.contains(WidgetState.pressed)) { + return const Size(2.0, 44.0); + } + return const Size(4.0, 44.0); + }); + } + + @override + double? get trackGap => 6.0; +} diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart index b5dac87..5e5f087 100644 --- a/lib/fragments/tools.dart +++ b/lib/fragments/tools.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'backup_and_recovery.dart'; +import 'developer.dart'; import 'theme.dart'; import 'package:path/path.dart' show dirname, join; @@ -54,11 +55,12 @@ class _ToolboxFragmentState extends ConsumerState { ); } - List _getOtherList() { + List _getOtherList(bool enableDeveloperMode) { return generateSection( title: appLocalizations.other, items: [ _DisclaimerItem(), + if (enableDeveloperMode) _DeveloperItem(), _InfoItem(), ], ); @@ -82,7 +84,11 @@ class _ToolboxFragmentState extends ConsumerState { @override Widget build(BuildContext context) { - ref.watch(appSettingProvider.select((state) => state.locale)); + final vm2 = ref.watch( + appSettingProvider.select( + (state) => VM2(a: state.locale, b: state.developerMode), + ), + ); final items = [ Consumer( builder: (_, ref, __) { @@ -99,7 +105,7 @@ class _ToolboxFragmentState extends ConsumerState { }, ), ..._getSettingList(), - ..._getOtherList(), + ..._getOtherList(vm2.b), ]; return ListView.builder( itemCount: items.length, @@ -297,3 +303,19 @@ class _InfoItem extends StatelessWidget { ); } } + +class _DeveloperItem extends StatelessWidget { + const _DeveloperItem(); + + @override + Widget build(BuildContext context) { + return ListItem.open( + leading: const Icon(Icons.developer_board), + title: Text(appLocalizations.developerMode), + delegate: OpenDelegate( + title: appLocalizations.developerMode, + widget: const DeveloperView(), + ), + ); + } +} diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index f7b0b08..72b6634 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -146,6 +146,7 @@ class MessageLookup extends MessageLookupByLibrary { "The current application is already the latest version", ), "checking": MessageLookupByLibrary.simpleMessage("Checking..."), + "clearData": MessageLookupByLibrary.simpleMessage("Clear Data"), "clipboardExport": MessageLookupByLibrary.simpleMessage("Export clipboard"), "clipboardImport": MessageLookupByLibrary.simpleMessage("Clipboard import"), "colorExists": MessageLookupByLibrary.simpleMessage( @@ -163,6 +164,7 @@ class MessageLookup extends MessageLookupByLibrary { "View current connections data", ), "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"), + "contactMe": MessageLookupByLibrary.simpleMessage("Contact me"), "content": MessageLookupByLibrary.simpleMessage("Content"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage( "Content cannot be empty", @@ -177,6 +179,7 @@ class MessageLookup extends MessageLookupByLibrary { "core": MessageLookupByLibrary.simpleMessage("Core"), "coreInfo": MessageLookupByLibrary.simpleMessage("Core info"), "country": MessageLookupByLibrary.simpleMessage("Country"), + "crashTest": MessageLookupByLibrary.simpleMessage("Crash test"), "create": MessageLookupByLibrary.simpleMessage("Create"), "cut": MessageLookupByLibrary.simpleMessage("Cut"), "dark": MessageLookupByLibrary.simpleMessage("Dark"), @@ -208,6 +211,10 @@ class MessageLookup extends MessageLookupByLibrary { "detectionTip": MessageLookupByLibrary.simpleMessage( "Relying on third-party api is for reference only", ), + "developerMode": MessageLookupByLibrary.simpleMessage("Developer mode"), + "developerModeEnableTip": MessageLookupByLibrary.simpleMessage( + "Developer mode is enabled.", + ), "direct": MessageLookupByLibrary.simpleMessage("Direct"), "disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( @@ -320,6 +327,7 @@ class MessageLookup extends MessageLookupByLibrary { "intelligentSelected": MessageLookupByLibrary.simpleMessage( "Intelligent selection", ), + "internet": MessageLookupByLibrary.simpleMessage("Internet"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), "ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( @@ -356,12 +364,17 @@ class MessageLookup extends MessageLookupByLibrary { ), "logs": MessageLookupByLibrary.simpleMessage("Logs"), "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), + "logsTest": MessageLookupByLibrary.simpleMessage("Logs test"), "loopback": MessageLookupByLibrary.simpleMessage("Loopback unlock tool"), "loopbackDesc": MessageLookupByLibrary.simpleMessage( "Used for UWP loopback unlocking", ), "loose": MessageLookupByLibrary.simpleMessage("Loose"), "memoryInfo": MessageLookupByLibrary.simpleMessage("Memory info"), + "messageTest": MessageLookupByLibrary.simpleMessage("Message test"), + "messageTestTip": MessageLookupByLibrary.simpleMessage( + "This is a message.", + ), "min": MessageLookupByLibrary.simpleMessage("Min"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on exit"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( @@ -399,6 +412,7 @@ class MessageLookup extends MessageLookupByLibrary { "noInfo": MessageLookupByLibrary.simpleMessage("No info"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"), "noNetwork": MessageLookupByLibrary.simpleMessage("No network"), + "noNetworkApp": MessageLookupByLibrary.simpleMessage("No network APP"), "noProxy": MessageLookupByLibrary.simpleMessage("No proxy"), "noProxyDesc": MessageLookupByLibrary.simpleMessage( "Please create a profile or add a valid profile", @@ -526,6 +540,15 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryProfiles": MessageLookupByLibrary.simpleMessage( "Only recovery profiles", ), + "recoveryStrategy": MessageLookupByLibrary.simpleMessage( + "Recovery strategy", + ), + "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage( + "Compatible", + ), + "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage( + "Override", + ), "recoverySuccess": MessageLookupByLibrary.simpleMessage("Recovery success"), "redo": MessageLookupByLibrary.simpleMessage("redo"), "regExp": MessageLookupByLibrary.simpleMessage("RegExp"), @@ -612,6 +635,7 @@ class MessageLookup extends MessageLookupByLibrary { "submit": MessageLookupByLibrary.simpleMessage("Submit"), "sync": MessageLookupByLibrary.simpleMessage("Sync"), "system": MessageLookupByLibrary.simpleMessage("System"), + "systemApp": MessageLookupByLibrary.simpleMessage("System APP"), "systemFont": MessageLookupByLibrary.simpleMessage("System font"), "systemProxy": MessageLookupByLibrary.simpleMessage("System proxy"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage( @@ -627,6 +651,7 @@ class MessageLookup extends MessageLookupByLibrary { "Enabling it will allow TCP concurrency", ), "testUrl": MessageLookupByLibrary.simpleMessage("Test url"), + "textScale": MessageLookupByLibrary.simpleMessage("Text Scaling"), "theme": MessageLookupByLibrary.simpleMessage("Theme"), "themeColor": MessageLookupByLibrary.simpleMessage("Theme color"), "themeDesc": MessageLookupByLibrary.simpleMessage( diff --git a/lib/l10n/intl/messages_ja.dart b/lib/l10n/intl/messages_ja.dart index 3c22730..d7bd7dc 100644 --- a/lib/l10n/intl/messages_ja.dart +++ b/lib/l10n/intl/messages_ja.dart @@ -104,6 +104,7 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdate": MessageLookupByLibrary.simpleMessage("更新を確認"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("アプリは最新版です"), "checking": MessageLookupByLibrary.simpleMessage("確認中..."), + "clearData": MessageLookupByLibrary.simpleMessage("データを消去"), "clipboardExport": MessageLookupByLibrary.simpleMessage("クリップボードにエクスポート"), "clipboardImport": MessageLookupByLibrary.simpleMessage("クリップボードからインポート"), "colorExists": MessageLookupByLibrary.simpleMessage("この色は既に存在します"), @@ -117,6 +118,7 @@ class MessageLookup extends MessageLookupByLibrary { "connections": MessageLookupByLibrary.simpleMessage("接続"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("現在の接続データを表示"), "connectivity": MessageLookupByLibrary.simpleMessage("接続性:"), + "contactMe": MessageLookupByLibrary.simpleMessage("連絡する"), "content": MessageLookupByLibrary.simpleMessage("内容"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容は必須です"), "contentScheme": MessageLookupByLibrary.simpleMessage("コンテンツテーマ"), @@ -127,6 +129,7 @@ class MessageLookup extends MessageLookupByLibrary { "core": MessageLookupByLibrary.simpleMessage("コア"), "coreInfo": MessageLookupByLibrary.simpleMessage("コア情報"), "country": MessageLookupByLibrary.simpleMessage("国"), + "crashTest": MessageLookupByLibrary.simpleMessage("クラッシュテスト"), "create": MessageLookupByLibrary.simpleMessage("作成"), "cut": MessageLookupByLibrary.simpleMessage("切り取り"), "dark": MessageLookupByLibrary.simpleMessage("ダーク"), @@ -150,6 +153,10 @@ class MessageLookup extends MessageLookupByLibrary { "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。", ), "detectionTip": MessageLookupByLibrary.simpleMessage("サードパーティAPIに依存(参考値)"), + "developerMode": MessageLookupByLibrary.simpleMessage("デベロッパーモード"), + "developerModeEnableTip": MessageLookupByLibrary.simpleMessage( + "デベロッパーモードが有効になりました。", + ), "direct": MessageLookupByLibrary.simpleMessage("ダイレクト"), "disclaimer": MessageLookupByLibrary.simpleMessage("免責事項"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( @@ -230,6 +237,7 @@ class MessageLookup extends MessageLookupByLibrary { "init": MessageLookupByLibrary.simpleMessage("初期化"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("正しいホットキーを入力"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("インテリジェント選択"), + "internet": MessageLookupByLibrary.simpleMessage("インターネット"), "intranetIP": MessageLookupByLibrary.simpleMessage("イントラネットIP"), "ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("有効化するとIPv6トラフィックを受信可能"), @@ -254,10 +262,13 @@ class MessageLookup extends MessageLookupByLibrary { "logcatDesc": MessageLookupByLibrary.simpleMessage("無効化するとログエントリを非表示"), "logs": MessageLookupByLibrary.simpleMessage("ログ"), "logsDesc": MessageLookupByLibrary.simpleMessage("ログキャプチャ記録"), + "logsTest": MessageLookupByLibrary.simpleMessage("ログテスト"), "loopback": MessageLookupByLibrary.simpleMessage("ループバック解除ツール"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("UWPループバック解除用"), "loose": MessageLookupByLibrary.simpleMessage("疎"), "memoryInfo": MessageLookupByLibrary.simpleMessage("メモリ情報"), + "messageTest": MessageLookupByLibrary.simpleMessage("メッセージテスト"), + "messageTestTip": MessageLookupByLibrary.simpleMessage("これはメッセージです。"), "min": MessageLookupByLibrary.simpleMessage("最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("終了時に最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( @@ -287,6 +298,7 @@ class MessageLookup extends MessageLookupByLibrary { "noInfo": MessageLookupByLibrary.simpleMessage("情報なし"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("追加情報なし"), "noNetwork": MessageLookupByLibrary.simpleMessage("ネットワークなし"), + "noNetworkApp": MessageLookupByLibrary.simpleMessage("ネットワークなしアプリ"), "noProxy": MessageLookupByLibrary.simpleMessage("プロキシなし"), "noProxyDesc": MessageLookupByLibrary.simpleMessage( "プロファイルを作成するか、有効なプロファイルを追加してください", @@ -384,6 +396,11 @@ class MessageLookup extends MessageLookupByLibrary { "recovery": MessageLookupByLibrary.simpleMessage("復元"), "recoveryAll": MessageLookupByLibrary.simpleMessage("全データ復元"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("プロファイルのみ復元"), + "recoveryStrategy": MessageLookupByLibrary.simpleMessage("リカバリー戦略"), + "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage("互換性"), + "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage( + "オーバーライド", + ), "recoverySuccess": MessageLookupByLibrary.simpleMessage("復元成功"), "redo": MessageLookupByLibrary.simpleMessage("やり直す"), "regExp": MessageLookupByLibrary.simpleMessage("正規表現"), @@ -452,6 +469,7 @@ class MessageLookup extends MessageLookupByLibrary { "submit": MessageLookupByLibrary.simpleMessage("送信"), "sync": MessageLookupByLibrary.simpleMessage("同期"), "system": MessageLookupByLibrary.simpleMessage("システム"), + "systemApp": MessageLookupByLibrary.simpleMessage("システムアプリ"), "systemFont": MessageLookupByLibrary.simpleMessage("システムフォント"), "systemProxy": MessageLookupByLibrary.simpleMessage("システムプロキシ"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage( @@ -463,6 +481,7 @@ class MessageLookup extends MessageLookupByLibrary { "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"), "testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"), + "textScale": MessageLookupByLibrary.simpleMessage("テキストスケーリング"), "theme": MessageLookupByLibrary.simpleMessage("テーマ"), "themeColor": MessageLookupByLibrary.simpleMessage("テーマカラー"), "themeDesc": MessageLookupByLibrary.simpleMessage("ダークモードの設定、色の調整"), diff --git a/lib/l10n/intl/messages_ru.dart b/lib/l10n/intl/messages_ru.dart index b53f49a..b28ad14 100644 --- a/lib/l10n/intl/messages_ru.dart +++ b/lib/l10n/intl/messages_ru.dart @@ -148,6 +148,7 @@ class MessageLookup extends MessageLookupByLibrary { "Текущее приложение уже является последней версией", ), "checking": MessageLookupByLibrary.simpleMessage("Проверка..."), + "clearData": MessageLookupByLibrary.simpleMessage("Очистить данные"), "clipboardExport": MessageLookupByLibrary.simpleMessage( "Экспорт в буфер обмена", ), @@ -169,6 +170,7 @@ class MessageLookup extends MessageLookupByLibrary { "Просмотр текущих данных о соединениях", ), "connectivity": MessageLookupByLibrary.simpleMessage("Связь:"), + "contactMe": MessageLookupByLibrary.simpleMessage("Свяжитесь со мной"), "content": MessageLookupByLibrary.simpleMessage("Содержание"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage( "Содержание не может быть пустым", @@ -183,6 +185,7 @@ class MessageLookup extends MessageLookupByLibrary { "core": MessageLookupByLibrary.simpleMessage("Ядро"), "coreInfo": MessageLookupByLibrary.simpleMessage("Информация о ядре"), "country": MessageLookupByLibrary.simpleMessage("Страна"), + "crashTest": MessageLookupByLibrary.simpleMessage("Тест на сбои"), "create": MessageLookupByLibrary.simpleMessage("Создать"), "cut": MessageLookupByLibrary.simpleMessage("Вырезать"), "dark": MessageLookupByLibrary.simpleMessage("Темный"), @@ -216,6 +219,10 @@ class MessageLookup extends MessageLookupByLibrary { "detectionTip": MessageLookupByLibrary.simpleMessage( "Опирается на сторонний API, только для справки", ), + "developerMode": MessageLookupByLibrary.simpleMessage("Режим разработчика"), + "developerModeEnableTip": MessageLookupByLibrary.simpleMessage( + "Режим разработчика активирован.", + ), "direct": MessageLookupByLibrary.simpleMessage("Прямой"), "disclaimer": MessageLookupByLibrary.simpleMessage( "Отказ от ответственности", @@ -342,6 +349,7 @@ class MessageLookup extends MessageLookupByLibrary { "intelligentSelected": MessageLookupByLibrary.simpleMessage( "Интеллектуальный выбор", ), + "internet": MessageLookupByLibrary.simpleMessage("Интернет"), "intranetIP": MessageLookupByLibrary.simpleMessage("Внутренний IP"), "ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( @@ -378,6 +386,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "logs": MessageLookupByLibrary.simpleMessage("Логи"), "logsDesc": MessageLookupByLibrary.simpleMessage("Записи захвата логов"), + "logsTest": MessageLookupByLibrary.simpleMessage("Тест журналов"), "loopback": MessageLookupByLibrary.simpleMessage( "Инструмент разблокировки Loopback", ), @@ -386,6 +395,10 @@ class MessageLookup extends MessageLookupByLibrary { ), "loose": MessageLookupByLibrary.simpleMessage("Свободный"), "memoryInfo": MessageLookupByLibrary.simpleMessage("Информация о памяти"), + "messageTest": MessageLookupByLibrary.simpleMessage( + "Тестирование сообщения", + ), + "messageTestTip": MessageLookupByLibrary.simpleMessage("Это сообщение."), "min": MessageLookupByLibrary.simpleMessage("Мин"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage( "Свернуть при выходе", @@ -427,6 +440,7 @@ class MessageLookup extends MessageLookupByLibrary { "Нет дополнительной информации", ), "noNetwork": MessageLookupByLibrary.simpleMessage("Нет сети"), + "noNetworkApp": MessageLookupByLibrary.simpleMessage("Приложение без сети"), "noProxy": MessageLookupByLibrary.simpleMessage("Нет прокси"), "noProxyDesc": MessageLookupByLibrary.simpleMessage( "Пожалуйста, создайте профиль или добавьте действительный профиль", @@ -560,6 +574,15 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryProfiles": MessageLookupByLibrary.simpleMessage( "Только восстановление профилей", ), + "recoveryStrategy": MessageLookupByLibrary.simpleMessage( + "Стратегия восстановления", + ), + "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage( + "Совместимый", + ), + "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage( + "Переопределение", + ), "recoverySuccess": MessageLookupByLibrary.simpleMessage( "Восстановление успешно", ), @@ -650,6 +673,7 @@ class MessageLookup extends MessageLookupByLibrary { "submit": MessageLookupByLibrary.simpleMessage("Отправить"), "sync": MessageLookupByLibrary.simpleMessage("Синхронизация"), "system": MessageLookupByLibrary.simpleMessage("Система"), + "systemApp": MessageLookupByLibrary.simpleMessage("Системное приложение"), "systemFont": MessageLookupByLibrary.simpleMessage("Системный шрифт"), "systemProxy": MessageLookupByLibrary.simpleMessage("Системный прокси"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage( @@ -665,6 +689,7 @@ class MessageLookup extends MessageLookupByLibrary { "Включение позволит использовать параллелизм TCP", ), "testUrl": MessageLookupByLibrary.simpleMessage("Тест URL"), + "textScale": MessageLookupByLibrary.simpleMessage("Масштабирование текста"), "theme": MessageLookupByLibrary.simpleMessage("Тема"), "themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"), "themeDesc": MessageLookupByLibrary.simpleMessage( diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index d9b875f..3a8d965 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -94,6 +94,7 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checking": MessageLookupByLibrary.simpleMessage("检测中..."), + "clearData": MessageLookupByLibrary.simpleMessage("清除数据"), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "colorExists": MessageLookupByLibrary.simpleMessage("该颜色已存在"), @@ -107,6 +108,7 @@ class MessageLookup extends MessageLookupByLibrary { "connections": MessageLookupByLibrary.simpleMessage("连接"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), + "contactMe": MessageLookupByLibrary.simpleMessage("联系我"), "content": MessageLookupByLibrary.simpleMessage("内容"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容不能为空"), "contentScheme": MessageLookupByLibrary.simpleMessage("内容主题"), @@ -117,6 +119,7 @@ class MessageLookup extends MessageLookupByLibrary { "core": MessageLookupByLibrary.simpleMessage("内核"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "country": MessageLookupByLibrary.simpleMessage("区域"), + "crashTest": MessageLookupByLibrary.simpleMessage("崩溃测试"), "create": MessageLookupByLibrary.simpleMessage("创建"), "cut": MessageLookupByLibrary.simpleMessage("剪切"), "dark": MessageLookupByLibrary.simpleMessage("深色"), @@ -136,6 +139,8 @@ class MessageLookup extends MessageLookupByLibrary { "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。", ), "detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api,仅供参考"), + "developerMode": MessageLookupByLibrary.simpleMessage("开发者模式"), + "developerModeEnableTip": MessageLookupByLibrary.simpleMessage("开发者模式已启用。"), "direct": MessageLookupByLibrary.simpleMessage("直连"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage( @@ -206,6 +211,7 @@ class MessageLookup extends MessageLookupByLibrary { "init": MessageLookupByLibrary.simpleMessage("初始化"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), + "internet": MessageLookupByLibrary.simpleMessage("互联网"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), @@ -228,10 +234,13 @@ class MessageLookup extends MessageLookupByLibrary { "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logs": MessageLookupByLibrary.simpleMessage("日志"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), + "logsTest": MessageLookupByLibrary.simpleMessage("日志测试"), "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "loose": MessageLookupByLibrary.simpleMessage("宽松"), "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), + "messageTest": MessageLookupByLibrary.simpleMessage("消息测试"), + "messageTestTip": MessageLookupByLibrary.simpleMessage("这是一条消息。"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), @@ -257,6 +266,7 @@ class MessageLookup extends MessageLookupByLibrary { "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), "noNetwork": MessageLookupByLibrary.simpleMessage("无网络"), + "noNetworkApp": MessageLookupByLibrary.simpleMessage("无网络应用"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), "noResolve": MessageLookupByLibrary.simpleMessage("不解析IP"), @@ -338,6 +348,9 @@ class MessageLookup extends MessageLookupByLibrary { "recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), + "recoveryStrategy": MessageLookupByLibrary.simpleMessage("恢复策略"), + "recoveryStrategy_compatible": MessageLookupByLibrary.simpleMessage("兼容"), + "recoveryStrategy_override": MessageLookupByLibrary.simpleMessage("覆盖"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "redo": MessageLookupByLibrary.simpleMessage("重做"), "regExp": MessageLookupByLibrary.simpleMessage("正则"), @@ -398,6 +411,7 @@ class MessageLookup extends MessageLookupByLibrary { "submit": MessageLookupByLibrary.simpleMessage("提交"), "sync": MessageLookupByLibrary.simpleMessage("同步"), "system": MessageLookupByLibrary.simpleMessage("系统"), + "systemApp": MessageLookupByLibrary.simpleMessage("系统应用"), "systemFont": MessageLookupByLibrary.simpleMessage("系统字体"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"), @@ -407,6 +421,7 @@ class MessageLookup extends MessageLookupByLibrary { "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "testUrl": MessageLookupByLibrary.simpleMessage("测速链接"), + "textScale": MessageLookupByLibrary.simpleMessage("文本缩放"), "theme": MessageLookupByLibrary.simpleMessage("主题"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index c6a4512..645a56d 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -3004,6 +3004,121 @@ class AppLocalizations { args: [], ); } + + /// `Developer mode` + String get developerMode { + return Intl.message( + 'Developer mode', + name: 'developerMode', + desc: '', + args: [], + ); + } + + /// `Developer mode is enabled.` + String get developerModeEnableTip { + return Intl.message( + 'Developer mode is enabled.', + name: 'developerModeEnableTip', + desc: '', + args: [], + ); + } + + /// `Message test` + String get messageTest { + return Intl.message( + 'Message test', + name: 'messageTest', + desc: '', + args: [], + ); + } + + /// `This is a message.` + String get messageTestTip { + return Intl.message( + 'This is a message.', + name: 'messageTestTip', + desc: '', + args: [], + ); + } + + /// `Crash test` + String get crashTest { + return Intl.message('Crash test', name: 'crashTest', desc: '', args: []); + } + + /// `Clear Data` + String get clearData { + return Intl.message('Clear Data', name: 'clearData', desc: '', args: []); + } + + /// `Text Scaling` + String get textScale { + return Intl.message('Text Scaling', name: 'textScale', desc: '', args: []); + } + + /// `Internet` + String get internet { + return Intl.message('Internet', name: 'internet', desc: '', args: []); + } + + /// `System APP` + String get systemApp { + return Intl.message('System APP', name: 'systemApp', desc: '', args: []); + } + + /// `No network APP` + String get noNetworkApp { + return Intl.message( + 'No network APP', + name: 'noNetworkApp', + desc: '', + args: [], + ); + } + + /// `Contact me` + String get contactMe { + return Intl.message('Contact me', name: 'contactMe', desc: '', args: []); + } + + /// `Recovery strategy` + String get recoveryStrategy { + return Intl.message( + 'Recovery strategy', + name: 'recoveryStrategy', + desc: '', + args: [], + ); + } + + /// `Override` + String get recoveryStrategy_override { + return Intl.message( + 'Override', + name: 'recoveryStrategy_override', + desc: '', + args: [], + ); + } + + /// `Compatible` + String get recoveryStrategy_compatible { + return Intl.message( + 'Compatible', + name: 'recoveryStrategy_compatible', + desc: '', + args: [], + ); + } + + /// `Logs test` + String get logsTest { + return Intl.message('Logs test', name: 'logsTest', desc: '', args: []); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 3d2e6fc..4bba192 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,12 +21,16 @@ import 'common/common.dart'; Future main() async { globalState.isService = false; WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (details) { + commonPrint.log(details.stack.toString()); + }; final version = await system.version; await clashCore.preload(); await globalState.initApp(version); await android?.init(); await window?.init(version); globalState.isPre = const String.fromEnvironment("APP_ENV") != 'stable'; + globalState.coreSHA256 = const String.fromEnvironment("CORE_SHA256"); HttpOverrides.global = FlClashHttpOverrides(); runApp(ProviderScope( child: const Application(), diff --git a/lib/manager/app_state_manager.dart b/lib/manager/app_state_manager.dart index e6ec465..a9e7d99 100644 --- a/lib/manager/app_state_manager.dart +++ b/lib/manager/app_state_manager.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class AppStateManager extends StatefulWidget { +class AppStateManager extends ConsumerStatefulWidget { final Widget child; const AppStateManager({ @@ -14,15 +16,22 @@ class AppStateManager extends StatefulWidget { }); @override - State createState() => _AppStateManagerState(); + ConsumerState createState() => _AppStateManagerState(); } -class _AppStateManagerState extends State +class _AppStateManagerState extends ConsumerState with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + ref.listenManual(layoutChangeProvider, (prev, next) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (prev != next) { + globalState.cacheHeightMap = {}; + } + }); + }); } @override diff --git a/lib/manager/clash_manager.dart b/lib/manager/clash_manager.dart index feec44c..d45d62a 100644 --- a/lib/manager/clash_manager.dart +++ b/lib/manager/clash_manager.dart @@ -71,22 +71,22 @@ class _ClashContainerState extends ConsumerState @override void onLog(Log log) { - ref.watch(logsProvider.notifier).addLog(log); + ref.read(logsProvider.notifier).addLog(log); if (log.logLevel == LogLevel.error) { - globalState.showNotifier(log.payload ?? ''); + globalState.showNotifier(log.payload); } super.onLog(log); } @override void onRequest(Connection connection) async { - ref.watch(requestsProvider.notifier).addRequest(connection); + ref.read(requestsProvider.notifier).addRequest(connection); super.onRequest(connection); } @override Future onLoaded(String providerName) async { - ref.watch(providersProvider.notifier).setProvider( + ref.read(providersProvider.notifier).setProvider( await clashCore.getExternalProvider( providerName, ), diff --git a/lib/manager/message_manager.dart b/lib/manager/message_manager.dart index cbf6df8..64cd9f0 100644 --- a/lib/manager/message_manager.dart +++ b/lib/manager/message_manager.dart @@ -21,7 +21,7 @@ class MessageManager extends StatefulWidget { class MessageManagerState extends State { final _messagesNotifier = ValueNotifier>([]); final List _bufferMessages = []; - Completer? _messageIngCompleter; + bool _pushing = false; @override void initState() { @@ -40,26 +40,27 @@ class MessageManagerState extends State { text: text, ); _bufferMessages.add(commonMessage); - _showMessage(); + await _showMessage(); } _showMessage() async { - if (_messageIngCompleter?.isCompleted == false) { + if (_pushing == true) { return; } + _pushing = true; while (_bufferMessages.isNotEmpty) { final commonMessage = _bufferMessages.removeAt(0); _messagesNotifier.value = List.from(_messagesNotifier.value) ..add( commonMessage, ); - - _messageIngCompleter = Completer(); await Future.delayed(Duration(seconds: 1)); Future.delayed(commonMessage.duration, () { _handleRemove(commonMessage); }); - _messageIngCompleter?.complete(true); + if (_bufferMessages.isEmpty) { + _pushing = false; + } } } @@ -77,41 +78,41 @@ class MessageManagerState extends State { valueListenable: _messagesNotifier, builder: (_, messages, __) { return FadeThroughBox( + margin: EdgeInsets.only( + top: kToolbarHeight + 8, + left: 12, + right: 12, + ), alignment: Alignment.topRight, child: messages.isEmpty ? SizedBox() : LayoutBuilder( - key: Key(messages.last.id), - builder: (_, constraints) { - return Card( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(12.0), - ), - ), - elevation: 10, - margin: EdgeInsets.only( - top: kToolbarHeight + 8, - left: 12, - right: 12, - ), - color: context.colorScheme.surfaceContainerHigh, - child: Container( - width: min( - constraints.maxWidth, - 400, - ), - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: 16, - ), - child: Text( - messages.last.text, - ), - ), - ); - }, + key: Key(messages.last.id), + builder: (_, constraints) { + return Card( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(12.0), + ), ), + elevation: 10, + color: context.colorScheme.surfaceContainerHigh, + child: Container( + width: min( + constraints.maxWidth, + 500, + ), + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + child: Text( + messages.last.text, + ), + ), + ); + }, + ), ); }, ), diff --git a/lib/manager/theme_manager.dart b/lib/manager/theme_manager.dart index feffea5..415364a 100644 --- a/lib/manager/theme_manager.dart +++ b/lib/manager/theme_manager.dart @@ -1,10 +1,13 @@ +import 'dart:math'; import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/measure.dart'; import 'package:fl_clash/common/theme.dart'; +import 'package:fl_clash/providers/config.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class ThemeManager extends StatelessWidget { +class ThemeManager extends ConsumerWidget { final Widget child; const ThemeManager({ @@ -13,14 +16,30 @@ class ThemeManager extends StatelessWidget { }); @override - Widget build(BuildContext context) { - globalState.measure = Measure.of(context); - globalState.theme = CommonTheme.of(context); + Widget build(BuildContext context, ref) { + final textScale = ref.read( + themeSettingProvider.select((state) => state.textScale), + ); + final double textScaleFactor = max( + min( + textScale.enable ? textScale.scale : defaultTextScaleFactor, + maxTextScale, + ), + minTextScale, + ); + + globalState.measure = Measure.of(context, textScaleFactor); + globalState.theme = CommonTheme.of(context, textScaleFactor); + final padding = MediaQuery.of(context).padding; + final height = MediaQuery.of(context).size.height; return MediaQuery( data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear( textScaleFactor, ), + padding: padding.copyWith( + top: padding.top > height * 0.3 ? 0.0 : padding.top, + ), ), child: LayoutBuilder( builder: (_, container) { diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index e2790f1..821f49a 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -25,7 +25,6 @@ class WindowManager extends ConsumerStatefulWidget { class _WindowContainerState extends ConsumerState with WindowListener, WindowExtListener { - @override Widget build(BuildContext context) { return widget.child; @@ -183,19 +182,23 @@ class _WindowHeaderState extends State { super.dispose(); } - _updateMaximized() { - isMaximizedNotifier.value = !isMaximizedNotifier.value; - switch (isMaximizedNotifier.value) { + _updateMaximized() async { + final isMaximized = await windowManager.isMaximized(); + switch (isMaximized) { case true: - windowManager.maximize(); + await windowManager.unmaximize(); + break; case false: - windowManager.unmaximize(); + await windowManager.maximize(); + break; } + isMaximizedNotifier.value = await windowManager.isMaximized(); } - _updatePin() { - isPinNotifier.value = !isPinNotifier.value; - windowManager.setAlwaysOnTop(isPinNotifier.value); + _updatePin() async { + final isAlwaysOnTop = await windowManager.isAlwaysOnTop(); + await windowManager.setAlwaysOnTop(!isAlwaysOnTop); + isPinNotifier.value = await windowManager.isAlwaysOnTop(); } _buildActions() { diff --git a/lib/models/app.dart b/lib/models/app.dart index f2d2ef9..5fccfbc 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -16,7 +16,6 @@ class AppState with _$AppState { @Default(false) bool isInit, @Default(PageLabel.dashboard) PageLabel pageLabel, @Default([]) List packages, - @Default(ColorSchemes()) ColorSchemes colorSchemes, @Default(0) int sortNum, required Size viewSize, @Default({}) DelayMap delayMap, diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 9b40834..086e21e 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -147,7 +147,8 @@ class Tun with _$Tun { const factory Tun({ @Default(false) bool enable, @Default(appName) String device, - @Default(TunStack.gvisor) TunStack stack, + @JsonKey(name: "auto-route") @Default(false) bool autoRoute, + @Default(TunStack.mixed) TunStack stack, @JsonKey(name: "dns-hijack") @Default(["any:53"]) List dnsHijack, @JsonKey(name: "route-address") @Default([]) List routeAddress, }) = _Tun; diff --git a/lib/models/common.dart b/lib/models/common.dart index df244c7..eb796ae 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -30,7 +30,8 @@ class Package with _$Package { const factory Package({ required String packageName, required String label, - required bool isSystem, + required bool system, + required bool internet, required int lastUpdateTime, }) = _Package; @@ -84,33 +85,33 @@ extension ConnectionExt on Connection { } } -@JsonSerializable() -class Log { - @JsonKey(name: "LogLevel") - LogLevel logLevel; - @JsonKey(name: "Payload") - String? payload; - DateTime _dateTime; +String _logDateTime(_) { + return DateTime.now().toString(); +} - Log({ - required this.logLevel, - this.payload, - }) : _dateTime = DateTime.now(); +// String _logId(_) { +// return utils.id; +// } - DateTime get dateTime => _dateTime; +@freezed +class Log with _$Log { + const factory Log({ + @JsonKey(name: "LogLevel") @Default(LogLevel.app) LogLevel logLevel, + @JsonKey(name: "Payload") @Default("") String payload, + @JsonKey(fromJson: _logDateTime) required String dateTime, + }) = _Log; - factory Log.fromJson(Map json) { - return _$LogFromJson(json); + factory Log.app( + String payload, + ) { + return Log( + payload: payload, + dateTime: _logDateTime(null), + // id: _logId(null), + ); } - Map toJson() { - return _$LogToJson(this); - } - - @override - String toString() { - return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}'; - } + factory Log.fromJson(Map json) => _$LogFromJson(json); } @freezed @@ -119,6 +120,7 @@ class LogsState with _$LogsState { @Default([]) List logs, @Default([]) List keywords, @Default("") String query, + @Default(false) bool loading, }) = _LogsState; } @@ -127,11 +129,10 @@ extension LogsStateExt on LogsState { final lowQuery = query.toLowerCase(); return logs.where( (log) { - final payload = log.payload?.toLowerCase(); + final payload = log.payload.toLowerCase(); final logLevelName = log.logLevel.name; return {logLevelName}.containsAll(keywords) && - ((payload?.contains(lowQuery) ?? false) || - logLevelName.contains(lowQuery)); + ((payload.contains(lowQuery)) || logLevelName.contains(lowQuery)); }, ).toList(); } @@ -143,6 +144,7 @@ class ConnectionsState with _$ConnectionsState { @Default([]) List connections, @Default([]) List keywords, @Default("") String query, + @Default(false) bool loading, }) = _ConnectionsState; } @@ -512,3 +514,17 @@ class PopupMenuItemData { final IconData? icon; final PopupMenuItemType? type; } + +@freezed +class TextPainterParams with _$TextPainterParams { + const factory TextPainterParams({ + required String? text, + required double? fontSize, + required double textScaleFactor, + @Default(double.infinity) double maxWidth, + int? maxLines, + }) = _TextPainterParams; + + factory TextPainterParams.fromJson(Map json) => + _$TextPainterParamsFromJson(json); +} diff --git a/lib/models/config.dart b/lib/models/config.dart index 18fed53..f541db2 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -37,7 +37,9 @@ const defaultNetworkProps = NetworkProps(); const defaultProxiesStyle = ProxiesStyle(); const defaultWindowProps = WindowProps(); const defaultAccessControl = AccessControl(); -const defaultThemeProps = ThemeProps(); +final defaultThemeProps = ThemeProps( + primaryColor: defaultPrimaryColor, +); const List defaultDashboardWidgets = [ DashboardWidget.networkSpeed, @@ -73,7 +75,7 @@ class AppSettingProps with _$AppSettingProps { @Default(false) bool autoLaunch, @Default(false) bool silentLaunch, @Default(false) bool autoRun, - @Default(true) bool openLogs, + @Default(false) bool openLogs, @Default(true) bool closeConnections, @Default(defaultTestUrl) String testUrl, @Default(true) bool isAnimateToPage, @@ -82,6 +84,8 @@ class AppSettingProps with _$AppSettingProps { @Default(false) bool disclaimerAccepted, @Default(true) bool minimizeOnExit, @Default(false) bool hidden, + @Default(false) bool developerMode, + @Default(RecoveryStrategy.compatible) RecoveryStrategy recoveryStrategy, }) = _AppSettingProps; factory AppSettingProps.fromJson(Map json) => @@ -103,6 +107,7 @@ class AccessControl with _$AccessControl { @Default([]) List rejectList, @Default(AccessSortType.none) AccessSortType sort, @Default(true) bool isFilterSystemApp, + @Default(true) bool isFilterNonInternetApp, }) = _AccessControl; factory AccessControl.fromJson(Map json) => @@ -170,18 +175,41 @@ class ProxiesStyle with _$ProxiesStyle { json == null ? defaultProxiesStyle : _$ProxiesStyleFromJson(json); } +@freezed +class TextScale with _$TextScale { + const factory TextScale({ + @Default(false) enable, + @Default(1.0) scale, + }) = _TextScale; + + factory TextScale.fromJson(Map json) => + _$TextScaleFromJson(json); +} + @freezed class ThemeProps with _$ThemeProps { const factory ThemeProps({ - @Default(defaultPrimaryColor) int? primaryColor, + int? primaryColor, @Default(defaultPrimaryColors) List primaryColors, @Default(ThemeMode.dark) ThemeMode themeMode, - @Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant, + @Default(DynamicSchemeVariant.content) DynamicSchemeVariant schemeVariant, @Default(false) bool pureBlack, + @Default(TextScale()) TextScale textScale, }) = _ThemeProps; - factory ThemeProps.fromJson(Map? json) => - json == null ? defaultThemeProps : _$ThemePropsFromJson(json); + factory ThemeProps.fromJson(Map json) => + _$ThemePropsFromJson(json); + + factory ThemeProps.safeFromJson(Map? json) { + if (json == null) { + return defaultThemeProps; + } + try { + return ThemeProps.fromJson(json); + } catch (_) { + return defaultThemeProps; + } + } } @freezed @@ -197,7 +225,7 @@ class Config with _$Config { DAV? dav, @Default(defaultNetworkProps) NetworkProps networkProps, @Default(defaultVpnProps) VpnProps vpnProps, - @Default(defaultThemeProps) ThemeProps themeProps, + @JsonKey(fromJson: ThemeProps.safeFromJson) required ThemeProps themeProps, @Default(defaultProxiesStyle) ProxiesStyle proxiesStyle, @Default(defaultWindowProps) WindowProps windowProps, @Default(defaultClashConfig) ClashConfig patchClashConfig, diff --git a/lib/models/generated/app.freezed.dart b/lib/models/generated/app.freezed.dart index 0623cb7..fd01404 100644 --- a/lib/models/generated/app.freezed.dart +++ b/lib/models/generated/app.freezed.dart @@ -19,7 +19,6 @@ mixin _$AppState { bool get isInit => throw _privateConstructorUsedError; PageLabel get pageLabel => throw _privateConstructorUsedError; List get packages => throw _privateConstructorUsedError; - ColorSchemes get colorSchemes => throw _privateConstructorUsedError; int get sortNum => throw _privateConstructorUsedError; Size get viewSize => throw _privateConstructorUsedError; Map> get delayMap => @@ -53,7 +52,6 @@ abstract class $AppStateCopyWith<$Res> { {bool isInit, PageLabel pageLabel, List packages, - ColorSchemes colorSchemes, int sortNum, Size viewSize, Map> delayMap, @@ -69,8 +67,6 @@ abstract class $AppStateCopyWith<$Res> { FixedList traffics, Traffic totalTraffic, bool needApply}); - - $ColorSchemesCopyWith<$Res> get colorSchemes; } /// @nodoc @@ -91,7 +87,6 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> Object? isInit = null, Object? pageLabel = null, Object? packages = null, - Object? colorSchemes = null, Object? sortNum = null, Object? viewSize = null, Object? delayMap = null, @@ -121,10 +116,6 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> ? _value.packages : packages // ignore: cast_nullable_to_non_nullable as List, - colorSchemes: null == colorSchemes - ? _value.colorSchemes - : colorSchemes // ignore: cast_nullable_to_non_nullable - as ColorSchemes, sortNum: null == sortNum ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable @@ -187,16 +178,6 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> as bool, ) as $Val); } - - /// Create a copy of AppState - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ColorSchemesCopyWith<$Res> get colorSchemes { - return $ColorSchemesCopyWith<$Res>(_value.colorSchemes, (value) { - return _then(_value.copyWith(colorSchemes: value) as $Val); - }); - } } /// @nodoc @@ -211,7 +192,6 @@ abstract class _$$AppStateImplCopyWith<$Res> {bool isInit, PageLabel pageLabel, List packages, - ColorSchemes colorSchemes, int sortNum, Size viewSize, Map> delayMap, @@ -227,9 +207,6 @@ abstract class _$$AppStateImplCopyWith<$Res> FixedList traffics, Traffic totalTraffic, bool needApply}); - - @override - $ColorSchemesCopyWith<$Res> get colorSchemes; } /// @nodoc @@ -248,7 +225,6 @@ class __$$AppStateImplCopyWithImpl<$Res> Object? isInit = null, Object? pageLabel = null, Object? packages = null, - Object? colorSchemes = null, Object? sortNum = null, Object? viewSize = null, Object? delayMap = null, @@ -278,10 +254,6 @@ class __$$AppStateImplCopyWithImpl<$Res> ? _value._packages : packages // ignore: cast_nullable_to_non_nullable as List, - colorSchemes: null == colorSchemes - ? _value.colorSchemes - : colorSchemes // ignore: cast_nullable_to_non_nullable - as ColorSchemes, sortNum: null == sortNum ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable @@ -353,7 +325,6 @@ class _$AppStateImpl implements _AppState { {this.isInit = false, this.pageLabel = PageLabel.dashboard, final List packages = const [], - this.colorSchemes = const ColorSchemes(), this.sortNum = 0, required this.viewSize, final Map> delayMap = const {}, @@ -389,9 +360,6 @@ class _$AppStateImpl implements _AppState { return EqualUnmodifiableListView(_packages); } - @override - @JsonKey() - final ColorSchemes colorSchemes; @override @JsonKey() final int sortNum; @@ -449,7 +417,7 @@ class _$AppStateImpl implements _AppState { @override String toString() { - return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, colorSchemes: $colorSchemes, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, needApply: $needApply)'; + return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, needApply: $needApply)'; } @override @@ -461,8 +429,6 @@ class _$AppStateImpl implements _AppState { (identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel) && const DeepCollectionEquality().equals(other._packages, _packages) && - (identical(other.colorSchemes, colorSchemes) || - other.colorSchemes == colorSchemes) && (identical(other.sortNum, sortNum) || other.sortNum == sortNum) && (identical(other.viewSize, viewSize) || other.viewSize == viewSize) && @@ -489,28 +455,26 @@ class _$AppStateImpl implements _AppState { } @override - int get hashCode => Object.hashAll([ - runtimeType, - isInit, - pageLabel, - const DeepCollectionEquality().hash(_packages), - colorSchemes, - sortNum, - viewSize, - const DeepCollectionEquality().hash(_delayMap), - const DeepCollectionEquality().hash(_groups), - checkIpNum, - brightness, - runTime, - const DeepCollectionEquality().hash(_providers), - localIp, - requests, - version, - logs, - traffics, - totalTraffic, - needApply - ]); + int get hashCode => Object.hash( + runtimeType, + isInit, + pageLabel, + const DeepCollectionEquality().hash(_packages), + sortNum, + viewSize, + const DeepCollectionEquality().hash(_delayMap), + const DeepCollectionEquality().hash(_groups), + checkIpNum, + brightness, + runTime, + const DeepCollectionEquality().hash(_providers), + localIp, + requests, + version, + logs, + traffics, + totalTraffic, + needApply); /// Create a copy of AppState /// with the given fields replaced by the non-null parameter values. @@ -526,7 +490,6 @@ abstract class _AppState implements AppState { {final bool isInit, final PageLabel pageLabel, final List packages, - final ColorSchemes colorSchemes, final int sortNum, required final Size viewSize, final Map> delayMap, @@ -550,8 +513,6 @@ abstract class _AppState implements AppState { @override List get packages; @override - ColorSchemes get colorSchemes; - @override int get sortNum; @override Size get viewSize; diff --git a/lib/models/generated/clash_config.freezed.dart b/lib/models/generated/clash_config.freezed.dart index 43c9b43..1f890b7 100644 --- a/lib/models/generated/clash_config.freezed.dart +++ b/lib/models/generated/clash_config.freezed.dart @@ -660,6 +660,8 @@ Tun _$TunFromJson(Map json) { mixin _$Tun { bool get enable => throw _privateConstructorUsedError; String get device => throw _privateConstructorUsedError; + @JsonKey(name: "auto-route") + bool get autoRoute => throw _privateConstructorUsedError; TunStack get stack => throw _privateConstructorUsedError; @JsonKey(name: "dns-hijack") List get dnsHijack => throw _privateConstructorUsedError; @@ -683,6 +685,7 @@ abstract class $TunCopyWith<$Res> { $Res call( {bool enable, String device, + @JsonKey(name: "auto-route") bool autoRoute, TunStack stack, @JsonKey(name: "dns-hijack") List dnsHijack, @JsonKey(name: "route-address") List routeAddress}); @@ -704,6 +707,7 @@ class _$TunCopyWithImpl<$Res, $Val extends Tun> implements $TunCopyWith<$Res> { $Res call({ Object? enable = null, Object? device = null, + Object? autoRoute = null, Object? stack = null, Object? dnsHijack = null, Object? routeAddress = null, @@ -717,6 +721,10 @@ class _$TunCopyWithImpl<$Res, $Val extends Tun> implements $TunCopyWith<$Res> { ? _value.device : device // ignore: cast_nullable_to_non_nullable as String, + autoRoute: null == autoRoute + ? _value.autoRoute + : autoRoute // ignore: cast_nullable_to_non_nullable + as bool, stack: null == stack ? _value.stack : stack // ignore: cast_nullable_to_non_nullable @@ -742,6 +750,7 @@ abstract class _$$TunImplCopyWith<$Res> implements $TunCopyWith<$Res> { $Res call( {bool enable, String device, + @JsonKey(name: "auto-route") bool autoRoute, TunStack stack, @JsonKey(name: "dns-hijack") List dnsHijack, @JsonKey(name: "route-address") List routeAddress}); @@ -760,6 +769,7 @@ class __$$TunImplCopyWithImpl<$Res> extends _$TunCopyWithImpl<$Res, _$TunImpl> $Res call({ Object? enable = null, Object? device = null, + Object? autoRoute = null, Object? stack = null, Object? dnsHijack = null, Object? routeAddress = null, @@ -773,6 +783,10 @@ class __$$TunImplCopyWithImpl<$Res> extends _$TunCopyWithImpl<$Res, _$TunImpl> ? _value.device : device // ignore: cast_nullable_to_non_nullable as String, + autoRoute: null == autoRoute + ? _value.autoRoute + : autoRoute // ignore: cast_nullable_to_non_nullable + as bool, stack: null == stack ? _value.stack : stack // ignore: cast_nullable_to_non_nullable @@ -795,7 +809,8 @@ class _$TunImpl implements _Tun { const _$TunImpl( {this.enable = false, this.device = appName, - this.stack = TunStack.gvisor, + @JsonKey(name: "auto-route") this.autoRoute = false, + this.stack = TunStack.mixed, @JsonKey(name: "dns-hijack") final List dnsHijack = const ["any:53"], @JsonKey(name: "route-address") @@ -813,6 +828,9 @@ class _$TunImpl implements _Tun { @JsonKey() final String device; @override + @JsonKey(name: "auto-route") + final bool autoRoute; + @override @JsonKey() final TunStack stack; final List _dnsHijack; @@ -835,7 +853,7 @@ class _$TunImpl implements _Tun { @override String toString() { - return 'Tun(enable: $enable, device: $device, stack: $stack, dnsHijack: $dnsHijack, routeAddress: $routeAddress)'; + return 'Tun(enable: $enable, device: $device, autoRoute: $autoRoute, stack: $stack, dnsHijack: $dnsHijack, routeAddress: $routeAddress)'; } @override @@ -845,6 +863,8 @@ class _$TunImpl implements _Tun { other is _$TunImpl && (identical(other.enable, enable) || other.enable == enable) && (identical(other.device, device) || other.device == device) && + (identical(other.autoRoute, autoRoute) || + other.autoRoute == autoRoute) && (identical(other.stack, stack) || other.stack == stack) && const DeepCollectionEquality() .equals(other._dnsHijack, _dnsHijack) && @@ -858,6 +878,7 @@ class _$TunImpl implements _Tun { runtimeType, enable, device, + autoRoute, stack, const DeepCollectionEquality().hash(_dnsHijack), const DeepCollectionEquality().hash(_routeAddress)); @@ -882,6 +903,7 @@ abstract class _Tun implements Tun { const factory _Tun( {final bool enable, final String device, + @JsonKey(name: "auto-route") final bool autoRoute, final TunStack stack, @JsonKey(name: "dns-hijack") final List dnsHijack, @JsonKey(name: "route-address") final List routeAddress}) = @@ -894,6 +916,9 @@ abstract class _Tun implements Tun { @override String get device; @override + @JsonKey(name: "auto-route") + bool get autoRoute; + @override TunStack get stack; @override @JsonKey(name: "dns-hijack") diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 954250b..b6ce121 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -66,8 +66,9 @@ Map _$$RuleProviderImplToJson(_$RuleProviderImpl instance) => _$TunImpl _$$TunImplFromJson(Map json) => _$TunImpl( enable: json['enable'] as bool? ?? false, device: json['device'] as String? ?? appName, + autoRoute: json['auto-route'] as bool? ?? false, stack: $enumDecodeNullable(_$TunStackEnumMap, json['stack']) ?? - TunStack.gvisor, + TunStack.mixed, dnsHijack: (json['dns-hijack'] as List?) ?.map((e) => e as String) .toList() ?? @@ -81,6 +82,7 @@ _$TunImpl _$$TunImplFromJson(Map json) => _$TunImpl( Map _$$TunImplToJson(_$TunImpl instance) => { 'enable': instance.enable, 'device': instance.device, + 'auto-route': instance.autoRoute, 'stack': _$TunStackEnumMap[instance.stack]!, 'dns-hijack': instance.dnsHijack, 'route-address': instance.routeAddress, @@ -342,6 +344,7 @@ const _$LogLevelEnumMap = { LogLevel.warning: 'warning', LogLevel.error: 'error', LogLevel.silent: 'silent', + LogLevel.app: 'app', }; const _$FindProcessModeEnumMap = { diff --git a/lib/models/generated/common.freezed.dart b/lib/models/generated/common.freezed.dart index 3c96ad5..5c892d1 100644 --- a/lib/models/generated/common.freezed.dart +++ b/lib/models/generated/common.freezed.dart @@ -289,7 +289,8 @@ Package _$PackageFromJson(Map json) { mixin _$Package { String get packageName => throw _privateConstructorUsedError; String get label => throw _privateConstructorUsedError; - bool get isSystem => throw _privateConstructorUsedError; + bool get system => throw _privateConstructorUsedError; + bool get internet => throw _privateConstructorUsedError; int get lastUpdateTime => throw _privateConstructorUsedError; /// Serializes this Package to a JSON map. @@ -307,7 +308,11 @@ abstract class $PackageCopyWith<$Res> { _$PackageCopyWithImpl<$Res, Package>; @useResult $Res call( - {String packageName, String label, bool isSystem, int lastUpdateTime}); + {String packageName, + String label, + bool system, + bool internet, + int lastUpdateTime}); } /// @nodoc @@ -327,7 +332,8 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package> $Res call({ Object? packageName = null, Object? label = null, - Object? isSystem = null, + Object? system = null, + Object? internet = null, Object? lastUpdateTime = null, }) { return _then(_value.copyWith( @@ -339,9 +345,13 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package> ? _value.label : label // ignore: cast_nullable_to_non_nullable as String, - isSystem: null == isSystem - ? _value.isSystem - : isSystem // ignore: cast_nullable_to_non_nullable + system: null == system + ? _value.system + : system // ignore: cast_nullable_to_non_nullable + as bool, + internet: null == internet + ? _value.internet + : internet // ignore: cast_nullable_to_non_nullable as bool, lastUpdateTime: null == lastUpdateTime ? _value.lastUpdateTime @@ -359,7 +369,11 @@ abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> { @override @useResult $Res call( - {String packageName, String label, bool isSystem, int lastUpdateTime}); + {String packageName, + String label, + bool system, + bool internet, + int lastUpdateTime}); } /// @nodoc @@ -377,7 +391,8 @@ class __$$PackageImplCopyWithImpl<$Res> $Res call({ Object? packageName = null, Object? label = null, - Object? isSystem = null, + Object? system = null, + Object? internet = null, Object? lastUpdateTime = null, }) { return _then(_$PackageImpl( @@ -389,9 +404,13 @@ class __$$PackageImplCopyWithImpl<$Res> ? _value.label : label // ignore: cast_nullable_to_non_nullable as String, - isSystem: null == isSystem - ? _value.isSystem - : isSystem // ignore: cast_nullable_to_non_nullable + system: null == system + ? _value.system + : system // ignore: cast_nullable_to_non_nullable + as bool, + internet: null == internet + ? _value.internet + : internet // ignore: cast_nullable_to_non_nullable as bool, lastUpdateTime: null == lastUpdateTime ? _value.lastUpdateTime @@ -407,7 +426,8 @@ class _$PackageImpl implements _Package { const _$PackageImpl( {required this.packageName, required this.label, - required this.isSystem, + required this.system, + required this.internet, required this.lastUpdateTime}); factory _$PackageImpl.fromJson(Map json) => @@ -418,13 +438,15 @@ class _$PackageImpl implements _Package { @override final String label; @override - final bool isSystem; + final bool system; + @override + final bool internet; @override final int lastUpdateTime; @override String toString() { - return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, lastUpdateTime: $lastUpdateTime)'; + return 'Package(packageName: $packageName, label: $label, system: $system, internet: $internet, lastUpdateTime: $lastUpdateTime)'; } @override @@ -435,16 +457,17 @@ class _$PackageImpl implements _Package { (identical(other.packageName, packageName) || other.packageName == packageName) && (identical(other.label, label) || other.label == label) && - (identical(other.isSystem, isSystem) || - other.isSystem == isSystem) && + (identical(other.system, system) || other.system == system) && + (identical(other.internet, internet) || + other.internet == internet) && (identical(other.lastUpdateTime, lastUpdateTime) || other.lastUpdateTime == lastUpdateTime)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, packageName, label, isSystem, lastUpdateTime); + int get hashCode => Object.hash( + runtimeType, packageName, label, system, internet, lastUpdateTime); /// Create a copy of Package /// with the given fields replaced by the non-null parameter values. @@ -466,7 +489,8 @@ abstract class _Package implements Package { const factory _Package( {required final String packageName, required final String label, - required final bool isSystem, + required final bool system, + required final bool internet, required final int lastUpdateTime}) = _$PackageImpl; factory _Package.fromJson(Map json) = _$PackageImpl.fromJson; @@ -476,7 +500,9 @@ abstract class _Package implements Package { @override String get label; @override - bool get isSystem; + bool get system; + @override + bool get internet; @override int get lastUpdateTime; @@ -1092,11 +1118,209 @@ abstract class _Connection implements Connection { throw _privateConstructorUsedError; } +Log _$LogFromJson(Map json) { + return _Log.fromJson(json); +} + +/// @nodoc +mixin _$Log { + @JsonKey(name: "LogLevel") + LogLevel get logLevel => throw _privateConstructorUsedError; + @JsonKey(name: "Payload") + String get payload => throw _privateConstructorUsedError; + @JsonKey(fromJson: _logDateTime) + String get dateTime => throw _privateConstructorUsedError; + + /// Serializes this Log to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Log + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LogCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LogCopyWith<$Res> { + factory $LogCopyWith(Log value, $Res Function(Log) then) = + _$LogCopyWithImpl<$Res, Log>; + @useResult + $Res call( + {@JsonKey(name: "LogLevel") LogLevel logLevel, + @JsonKey(name: "Payload") String payload, + @JsonKey(fromJson: _logDateTime) String dateTime}); +} + +/// @nodoc +class _$LogCopyWithImpl<$Res, $Val extends Log> implements $LogCopyWith<$Res> { + _$LogCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Log + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? logLevel = null, + Object? payload = null, + Object? dateTime = null, + }) { + return _then(_value.copyWith( + logLevel: null == logLevel + ? _value.logLevel + : logLevel // ignore: cast_nullable_to_non_nullable + as LogLevel, + payload: null == payload + ? _value.payload + : payload // ignore: cast_nullable_to_non_nullable + as String, + dateTime: null == dateTime + ? _value.dateTime + : dateTime // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LogImplCopyWith<$Res> implements $LogCopyWith<$Res> { + factory _$$LogImplCopyWith(_$LogImpl value, $Res Function(_$LogImpl) then) = + __$$LogImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: "LogLevel") LogLevel logLevel, + @JsonKey(name: "Payload") String payload, + @JsonKey(fromJson: _logDateTime) String dateTime}); +} + +/// @nodoc +class __$$LogImplCopyWithImpl<$Res> extends _$LogCopyWithImpl<$Res, _$LogImpl> + implements _$$LogImplCopyWith<$Res> { + __$$LogImplCopyWithImpl(_$LogImpl _value, $Res Function(_$LogImpl) _then) + : super(_value, _then); + + /// Create a copy of Log + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? logLevel = null, + Object? payload = null, + Object? dateTime = null, + }) { + return _then(_$LogImpl( + logLevel: null == logLevel + ? _value.logLevel + : logLevel // ignore: cast_nullable_to_non_nullable + as LogLevel, + payload: null == payload + ? _value.payload + : payload // ignore: cast_nullable_to_non_nullable + as String, + dateTime: null == dateTime + ? _value.dateTime + : dateTime // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LogImpl implements _Log { + const _$LogImpl( + {@JsonKey(name: "LogLevel") this.logLevel = LogLevel.app, + @JsonKey(name: "Payload") this.payload = "", + @JsonKey(fromJson: _logDateTime) required this.dateTime}); + + factory _$LogImpl.fromJson(Map json) => + _$$LogImplFromJson(json); + + @override + @JsonKey(name: "LogLevel") + final LogLevel logLevel; + @override + @JsonKey(name: "Payload") + final String payload; + @override + @JsonKey(fromJson: _logDateTime) + final String dateTime; + + @override + String toString() { + return 'Log(logLevel: $logLevel, payload: $payload, dateTime: $dateTime)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LogImpl && + (identical(other.logLevel, logLevel) || + other.logLevel == logLevel) && + (identical(other.payload, payload) || other.payload == payload) && + (identical(other.dateTime, dateTime) || + other.dateTime == dateTime)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, logLevel, payload, dateTime); + + /// Create a copy of Log + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LogImplCopyWith<_$LogImpl> get copyWith => + __$$LogImplCopyWithImpl<_$LogImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LogImplToJson( + this, + ); + } +} + +abstract class _Log implements Log { + const factory _Log( + {@JsonKey(name: "LogLevel") final LogLevel logLevel, + @JsonKey(name: "Payload") final String payload, + @JsonKey(fromJson: _logDateTime) required final String dateTime}) = + _$LogImpl; + + factory _Log.fromJson(Map json) = _$LogImpl.fromJson; + + @override + @JsonKey(name: "LogLevel") + LogLevel get logLevel; + @override + @JsonKey(name: "Payload") + String get payload; + @override + @JsonKey(fromJson: _logDateTime) + String get dateTime; + + /// Create a copy of Log + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LogImplCopyWith<_$LogImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$LogsState { List get logs => throw _privateConstructorUsedError; List get keywords => throw _privateConstructorUsedError; String get query => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; /// Create a copy of LogsState /// with the given fields replaced by the non-null parameter values. @@ -1110,7 +1334,8 @@ abstract class $LogsStateCopyWith<$Res> { factory $LogsStateCopyWith(LogsState value, $Res Function(LogsState) then) = _$LogsStateCopyWithImpl<$Res, LogsState>; @useResult - $Res call({List logs, List keywords, String query}); + $Res call( + {List logs, List keywords, String query, bool loading}); } /// @nodoc @@ -1131,6 +1356,7 @@ class _$LogsStateCopyWithImpl<$Res, $Val extends LogsState> Object? logs = null, Object? keywords = null, Object? query = null, + Object? loading = null, }) { return _then(_value.copyWith( logs: null == logs @@ -1145,6 +1371,10 @@ class _$LogsStateCopyWithImpl<$Res, $Val extends LogsState> ? _value.query : query // ignore: cast_nullable_to_non_nullable as String, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -1157,7 +1387,8 @@ abstract class _$$LogsStateImplCopyWith<$Res> __$$LogsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({List logs, List keywords, String query}); + $Res call( + {List logs, List keywords, String query, bool loading}); } /// @nodoc @@ -1176,6 +1407,7 @@ class __$$LogsStateImplCopyWithImpl<$Res> Object? logs = null, Object? keywords = null, Object? query = null, + Object? loading = null, }) { return _then(_$LogsStateImpl( logs: null == logs @@ -1190,6 +1422,10 @@ class __$$LogsStateImplCopyWithImpl<$Res> ? _value.query : query // ignore: cast_nullable_to_non_nullable as String, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -1200,7 +1436,8 @@ class _$LogsStateImpl implements _LogsState { const _$LogsStateImpl( {final List logs = const [], final List keywords = const [], - this.query = ""}) + this.query = "", + this.loading = false}) : _logs = logs, _keywords = keywords; @@ -1225,10 +1462,13 @@ class _$LogsStateImpl implements _LogsState { @override @JsonKey() final String query; + @override + @JsonKey() + final bool loading; @override String toString() { - return 'LogsState(logs: $logs, keywords: $keywords, query: $query)'; + return 'LogsState(logs: $logs, keywords: $keywords, query: $query, loading: $loading)'; } @override @@ -1238,7 +1478,8 @@ class _$LogsStateImpl implements _LogsState { other is _$LogsStateImpl && const DeepCollectionEquality().equals(other._logs, _logs) && const DeepCollectionEquality().equals(other._keywords, _keywords) && - (identical(other.query, query) || other.query == query)); + (identical(other.query, query) || other.query == query) && + (identical(other.loading, loading) || other.loading == loading)); } @override @@ -1246,7 +1487,8 @@ class _$LogsStateImpl implements _LogsState { runtimeType, const DeepCollectionEquality().hash(_logs), const DeepCollectionEquality().hash(_keywords), - query); + query, + loading); /// Create a copy of LogsState /// with the given fields replaced by the non-null parameter values. @@ -1261,7 +1503,8 @@ abstract class _LogsState implements LogsState { const factory _LogsState( {final List logs, final List keywords, - final String query}) = _$LogsStateImpl; + final String query, + final bool loading}) = _$LogsStateImpl; @override List get logs; @@ -1269,6 +1512,8 @@ abstract class _LogsState implements LogsState { List get keywords; @override String get query; + @override + bool get loading; /// Create a copy of LogsState /// with the given fields replaced by the non-null parameter values. @@ -1283,6 +1528,7 @@ mixin _$ConnectionsState { List get connections => throw _privateConstructorUsedError; List get keywords => throw _privateConstructorUsedError; String get query => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; /// Create a copy of ConnectionsState /// with the given fields replaced by the non-null parameter values. @@ -1298,7 +1544,10 @@ abstract class $ConnectionsStateCopyWith<$Res> { _$ConnectionsStateCopyWithImpl<$Res, ConnectionsState>; @useResult $Res call( - {List connections, List keywords, String query}); + {List connections, + List keywords, + String query, + bool loading}); } /// @nodoc @@ -1319,6 +1568,7 @@ class _$ConnectionsStateCopyWithImpl<$Res, $Val extends ConnectionsState> Object? connections = null, Object? keywords = null, Object? query = null, + Object? loading = null, }) { return _then(_value.copyWith( connections: null == connections @@ -1333,6 +1583,10 @@ class _$ConnectionsStateCopyWithImpl<$Res, $Val extends ConnectionsState> ? _value.query : query // ignore: cast_nullable_to_non_nullable as String, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -1346,7 +1600,10 @@ abstract class _$$ConnectionsStateImplCopyWith<$Res> @override @useResult $Res call( - {List connections, List keywords, String query}); + {List connections, + List keywords, + String query, + bool loading}); } /// @nodoc @@ -1365,6 +1622,7 @@ class __$$ConnectionsStateImplCopyWithImpl<$Res> Object? connections = null, Object? keywords = null, Object? query = null, + Object? loading = null, }) { return _then(_$ConnectionsStateImpl( connections: null == connections @@ -1379,6 +1637,10 @@ class __$$ConnectionsStateImplCopyWithImpl<$Res> ? _value.query : query // ignore: cast_nullable_to_non_nullable as String, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -1389,7 +1651,8 @@ class _$ConnectionsStateImpl implements _ConnectionsState { const _$ConnectionsStateImpl( {final List connections = const [], final List keywords = const [], - this.query = ""}) + this.query = "", + this.loading = false}) : _connections = connections, _keywords = keywords; @@ -1414,10 +1677,13 @@ class _$ConnectionsStateImpl implements _ConnectionsState { @override @JsonKey() final String query; + @override + @JsonKey() + final bool loading; @override String toString() { - return 'ConnectionsState(connections: $connections, keywords: $keywords, query: $query)'; + return 'ConnectionsState(connections: $connections, keywords: $keywords, query: $query, loading: $loading)'; } @override @@ -1428,7 +1694,8 @@ class _$ConnectionsStateImpl implements _ConnectionsState { const DeepCollectionEquality() .equals(other._connections, _connections) && const DeepCollectionEquality().equals(other._keywords, _keywords) && - (identical(other.query, query) || other.query == query)); + (identical(other.query, query) || other.query == query) && + (identical(other.loading, loading) || other.loading == loading)); } @override @@ -1436,7 +1703,8 @@ class _$ConnectionsStateImpl implements _ConnectionsState { runtimeType, const DeepCollectionEquality().hash(_connections), const DeepCollectionEquality().hash(_keywords), - query); + query, + loading); /// Create a copy of ConnectionsState /// with the given fields replaced by the non-null parameter values. @@ -1452,7 +1720,8 @@ abstract class _ConnectionsState implements ConnectionsState { const factory _ConnectionsState( {final List connections, final List keywords, - final String query}) = _$ConnectionsStateImpl; + final String query, + final bool loading}) = _$ConnectionsStateImpl; @override List get connections; @@ -1460,6 +1729,8 @@ abstract class _ConnectionsState implements ConnectionsState { List get keywords; @override String get query; + @override + bool get loading; /// Create a copy of ConnectionsState /// with the given fields replaced by the non-null parameter values. @@ -2955,3 +3226,243 @@ abstract class _Field implements Field { _$$FieldImplCopyWith<_$FieldImpl> get copyWith => throw _privateConstructorUsedError; } + +TextPainterParams _$TextPainterParamsFromJson(Map json) { + return _TextPainterParams.fromJson(json); +} + +/// @nodoc +mixin _$TextPainterParams { + String? get text => throw _privateConstructorUsedError; + double? get fontSize => throw _privateConstructorUsedError; + double get textScaleFactor => throw _privateConstructorUsedError; + double get maxWidth => throw _privateConstructorUsedError; + int? get maxLines => throw _privateConstructorUsedError; + + /// Serializes this TextPainterParams to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TextPainterParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TextPainterParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TextPainterParamsCopyWith<$Res> { + factory $TextPainterParamsCopyWith( + TextPainterParams value, $Res Function(TextPainterParams) then) = + _$TextPainterParamsCopyWithImpl<$Res, TextPainterParams>; + @useResult + $Res call( + {String? text, + double? fontSize, + double textScaleFactor, + double maxWidth, + int? maxLines}); +} + +/// @nodoc +class _$TextPainterParamsCopyWithImpl<$Res, $Val extends TextPainterParams> + implements $TextPainterParamsCopyWith<$Res> { + _$TextPainterParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TextPainterParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? text = freezed, + Object? fontSize = freezed, + Object? textScaleFactor = null, + Object? maxWidth = null, + Object? maxLines = freezed, + }) { + return _then(_value.copyWith( + text: freezed == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String?, + fontSize: freezed == fontSize + ? _value.fontSize + : fontSize // ignore: cast_nullable_to_non_nullable + as double?, + textScaleFactor: null == textScaleFactor + ? _value.textScaleFactor + : textScaleFactor // ignore: cast_nullable_to_non_nullable + as double, + maxWidth: null == maxWidth + ? _value.maxWidth + : maxWidth // ignore: cast_nullable_to_non_nullable + as double, + maxLines: freezed == maxLines + ? _value.maxLines + : maxLines // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TextPainterParamsImplCopyWith<$Res> + implements $TextPainterParamsCopyWith<$Res> { + factory _$$TextPainterParamsImplCopyWith(_$TextPainterParamsImpl value, + $Res Function(_$TextPainterParamsImpl) then) = + __$$TextPainterParamsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? text, + double? fontSize, + double textScaleFactor, + double maxWidth, + int? maxLines}); +} + +/// @nodoc +class __$$TextPainterParamsImplCopyWithImpl<$Res> + extends _$TextPainterParamsCopyWithImpl<$Res, _$TextPainterParamsImpl> + implements _$$TextPainterParamsImplCopyWith<$Res> { + __$$TextPainterParamsImplCopyWithImpl(_$TextPainterParamsImpl _value, + $Res Function(_$TextPainterParamsImpl) _then) + : super(_value, _then); + + /// Create a copy of TextPainterParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? text = freezed, + Object? fontSize = freezed, + Object? textScaleFactor = null, + Object? maxWidth = null, + Object? maxLines = freezed, + }) { + return _then(_$TextPainterParamsImpl( + text: freezed == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String?, + fontSize: freezed == fontSize + ? _value.fontSize + : fontSize // ignore: cast_nullable_to_non_nullable + as double?, + textScaleFactor: null == textScaleFactor + ? _value.textScaleFactor + : textScaleFactor // ignore: cast_nullable_to_non_nullable + as double, + maxWidth: null == maxWidth + ? _value.maxWidth + : maxWidth // ignore: cast_nullable_to_non_nullable + as double, + maxLines: freezed == maxLines + ? _value.maxLines + : maxLines // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TextPainterParamsImpl implements _TextPainterParams { + const _$TextPainterParamsImpl( + {required this.text, + required this.fontSize, + required this.textScaleFactor, + this.maxWidth = double.infinity, + this.maxLines}); + + factory _$TextPainterParamsImpl.fromJson(Map json) => + _$$TextPainterParamsImplFromJson(json); + + @override + final String? text; + @override + final double? fontSize; + @override + final double textScaleFactor; + @override + @JsonKey() + final double maxWidth; + @override + final int? maxLines; + + @override + String toString() { + return 'TextPainterParams(text: $text, fontSize: $fontSize, textScaleFactor: $textScaleFactor, maxWidth: $maxWidth, maxLines: $maxLines)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TextPainterParamsImpl && + (identical(other.text, text) || other.text == text) && + (identical(other.fontSize, fontSize) || + other.fontSize == fontSize) && + (identical(other.textScaleFactor, textScaleFactor) || + other.textScaleFactor == textScaleFactor) && + (identical(other.maxWidth, maxWidth) || + other.maxWidth == maxWidth) && + (identical(other.maxLines, maxLines) || + other.maxLines == maxLines)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, text, fontSize, textScaleFactor, maxWidth, maxLines); + + /// Create a copy of TextPainterParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TextPainterParamsImplCopyWith<_$TextPainterParamsImpl> get copyWith => + __$$TextPainterParamsImplCopyWithImpl<_$TextPainterParamsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$TextPainterParamsImplToJson( + this, + ); + } +} + +abstract class _TextPainterParams implements TextPainterParams { + const factory _TextPainterParams( + {required final String? text, + required final double? fontSize, + required final double textScaleFactor, + final double maxWidth, + final int? maxLines}) = _$TextPainterParamsImpl; + + factory _TextPainterParams.fromJson(Map json) = + _$TextPainterParamsImpl.fromJson; + + @override + String? get text; + @override + double? get fontSize; + @override + double get textScaleFactor; + @override + double get maxWidth; + @override + int? get maxLines; + + /// Create a copy of TextPainterParams + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TextPainterParamsImplCopyWith<_$TextPainterParamsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/common.g.dart b/lib/models/generated/common.g.dart index 52aa4ef..665f13b 100644 --- a/lib/models/generated/common.g.dart +++ b/lib/models/generated/common.g.dart @@ -6,29 +6,12 @@ part of '../common.dart'; // JsonSerializableGenerator // ************************************************************************** -Log _$LogFromJson(Map json) => Log( - logLevel: $enumDecode(_$LogLevelEnumMap, json['LogLevel']), - payload: json['Payload'] as String?, - ); - -Map _$LogToJson(Log instance) => { - 'LogLevel': _$LogLevelEnumMap[instance.logLevel]!, - 'Payload': instance.payload, - }; - -const _$LogLevelEnumMap = { - LogLevel.debug: 'debug', - LogLevel.info: 'info', - LogLevel.warning: 'warning', - LogLevel.error: 'error', - LogLevel.silent: 'silent', -}; - _$PackageImpl _$$PackageImplFromJson(Map json) => _$PackageImpl( packageName: json['packageName'] as String, label: json['label'] as String, - isSystem: json['isSystem'] as bool, + system: json['system'] as bool, + internet: json['internet'] as bool, lastUpdateTime: (json['lastUpdateTime'] as num).toInt(), ); @@ -36,7 +19,8 @@ Map _$$PackageImplToJson(_$PackageImpl instance) => { 'packageName': instance.packageName, 'label': instance.label, - 'isSystem': instance.isSystem, + 'system': instance.system, + 'internet': instance.internet, 'lastUpdateTime': instance.lastUpdateTime, }; @@ -87,6 +71,28 @@ Map _$$ConnectionImplToJson(_$ConnectionImpl instance) => 'chains': instance.chains, }; +_$LogImpl _$$LogImplFromJson(Map json) => _$LogImpl( + logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['LogLevel']) ?? + LogLevel.app, + payload: json['Payload'] as String? ?? "", + dateTime: _logDateTime(json['dateTime']), + ); + +Map _$$LogImplToJson(_$LogImpl instance) => { + 'LogLevel': _$LogLevelEnumMap[instance.logLevel]!, + 'Payload': instance.payload, + 'dateTime': instance.dateTime, + }; + +const _$LogLevelEnumMap = { + LogLevel.debug: 'debug', + LogLevel.info: 'info', + LogLevel.warning: 'warning', + LogLevel.error: 'error', + LogLevel.silent: 'silent', + LogLevel.app: 'app', +}; + _$DAVImpl _$$DAVImplFromJson(Map json) => _$DAVImpl( uri: json['uri'] as String, user: json['user'] as String, @@ -192,3 +198,23 @@ const _$KeyboardModifierEnumMap = { KeyboardModifier.meta: 'meta', KeyboardModifier.shift: 'shift', }; + +_$TextPainterParamsImpl _$$TextPainterParamsImplFromJson( + Map json) => + _$TextPainterParamsImpl( + text: json['text'] as String?, + fontSize: (json['fontSize'] as num?)?.toDouble(), + textScaleFactor: (json['textScaleFactor'] as num).toDouble(), + maxWidth: (json['maxWidth'] as num?)?.toDouble() ?? double.infinity, + maxLines: (json['maxLines'] as num?)?.toInt(), + ); + +Map _$$TextPainterParamsImplToJson( + _$TextPainterParamsImpl instance) => + { + 'text': instance.text, + 'fontSize': instance.fontSize, + 'textScaleFactor': instance.textScaleFactor, + 'maxWidth': instance.maxWidth, + 'maxLines': instance.maxLines, + }; diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 384d607..92b7241 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -37,6 +37,8 @@ mixin _$AppSettingProps { bool get disclaimerAccepted => throw _privateConstructorUsedError; bool get minimizeOnExit => throw _privateConstructorUsedError; bool get hidden => throw _privateConstructorUsedError; + bool get developerMode => throw _privateConstructorUsedError; + RecoveryStrategy get recoveryStrategy => throw _privateConstructorUsedError; /// Serializes this AppSettingProps to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -70,7 +72,9 @@ abstract class $AppSettingPropsCopyWith<$Res> { bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, - bool hidden}); + bool hidden, + bool developerMode, + RecoveryStrategy recoveryStrategy}); } /// @nodoc @@ -103,6 +107,8 @@ class _$AppSettingPropsCopyWithImpl<$Res, $Val extends AppSettingProps> Object? disclaimerAccepted = null, Object? minimizeOnExit = null, Object? hidden = null, + Object? developerMode = null, + Object? recoveryStrategy = null, }) { return _then(_value.copyWith( locale: freezed == locale @@ -165,6 +171,14 @@ class _$AppSettingPropsCopyWithImpl<$Res, $Val extends AppSettingProps> ? _value.hidden : hidden // ignore: cast_nullable_to_non_nullable as bool, + developerMode: null == developerMode + ? _value.developerMode + : developerMode // ignore: cast_nullable_to_non_nullable + as bool, + recoveryStrategy: null == recoveryStrategy + ? _value.recoveryStrategy + : recoveryStrategy // ignore: cast_nullable_to_non_nullable + as RecoveryStrategy, ) as $Val); } } @@ -193,7 +207,9 @@ abstract class _$$AppSettingPropsImplCopyWith<$Res> bool showLabel, bool disclaimerAccepted, bool minimizeOnExit, - bool hidden}); + bool hidden, + bool developerMode, + RecoveryStrategy recoveryStrategy}); } /// @nodoc @@ -224,6 +240,8 @@ class __$$AppSettingPropsImplCopyWithImpl<$Res> Object? disclaimerAccepted = null, Object? minimizeOnExit = null, Object? hidden = null, + Object? developerMode = null, + Object? recoveryStrategy = null, }) { return _then(_$AppSettingPropsImpl( locale: freezed == locale @@ -286,6 +304,14 @@ class __$$AppSettingPropsImplCopyWithImpl<$Res> ? _value.hidden : hidden // ignore: cast_nullable_to_non_nullable as bool, + developerMode: null == developerMode + ? _value.developerMode + : developerMode // ignore: cast_nullable_to_non_nullable + as bool, + recoveryStrategy: null == recoveryStrategy + ? _value.recoveryStrategy + : recoveryStrategy // ignore: cast_nullable_to_non_nullable + as RecoveryStrategy, )); } } @@ -301,7 +327,7 @@ class _$AppSettingPropsImpl implements _AppSettingProps { this.autoLaunch = false, this.silentLaunch = false, this.autoRun = false, - this.openLogs = true, + this.openLogs = false, this.closeConnections = true, this.testUrl = defaultTestUrl, this.isAnimateToPage = true, @@ -309,7 +335,9 @@ class _$AppSettingPropsImpl implements _AppSettingProps { this.showLabel = false, this.disclaimerAccepted = false, this.minimizeOnExit = true, - this.hidden = false}) + this.hidden = false, + this.developerMode = false, + this.recoveryStrategy = RecoveryStrategy.compatible}) : _dashboardWidgets = dashboardWidgets; factory _$AppSettingPropsImpl.fromJson(Map json) => @@ -366,10 +394,16 @@ class _$AppSettingPropsImpl implements _AppSettingProps { @override @JsonKey() final bool hidden; + @override + @JsonKey() + final bool developerMode; + @override + @JsonKey() + final RecoveryStrategy 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)'; + 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)'; } @override @@ -402,7 +436,11 @@ class _$AppSettingPropsImpl implements _AppSettingProps { other.disclaimerAccepted == disclaimerAccepted) && (identical(other.minimizeOnExit, minimizeOnExit) || other.minimizeOnExit == minimizeOnExit) && - (identical(other.hidden, hidden) || other.hidden == hidden)); + (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) @@ -423,7 +461,9 @@ class _$AppSettingPropsImpl implements _AppSettingProps { showLabel, disclaimerAccepted, minimizeOnExit, - hidden); + hidden, + developerMode, + recoveryStrategy); /// Create a copy of AppSettingProps /// with the given fields replaced by the non-null parameter values. @@ -459,7 +499,9 @@ abstract class _AppSettingProps implements AppSettingProps { final bool showLabel, final bool disclaimerAccepted, final bool minimizeOnExit, - final bool hidden}) = _$AppSettingPropsImpl; + final bool hidden, + final bool developerMode, + final RecoveryStrategy recoveryStrategy}) = _$AppSettingPropsImpl; factory _AppSettingProps.fromJson(Map json) = _$AppSettingPropsImpl.fromJson; @@ -495,6 +537,10 @@ abstract class _AppSettingProps implements AppSettingProps { bool get minimizeOnExit; @override bool get hidden; + @override + bool get developerMode; + @override + RecoveryStrategy get recoveryStrategy; /// Create a copy of AppSettingProps /// with the given fields replaced by the non-null parameter values. @@ -516,6 +562,7 @@ mixin _$AccessControl { List get rejectList => throw _privateConstructorUsedError; AccessSortType get sort => throw _privateConstructorUsedError; bool get isFilterSystemApp => throw _privateConstructorUsedError; + bool get isFilterNonInternetApp => throw _privateConstructorUsedError; /// Serializes this AccessControl to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -539,7 +586,8 @@ abstract class $AccessControlCopyWith<$Res> { List acceptList, List rejectList, AccessSortType sort, - bool isFilterSystemApp}); + bool isFilterSystemApp, + bool isFilterNonInternetApp}); } /// @nodoc @@ -563,6 +611,7 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl> Object? rejectList = null, Object? sort = null, Object? isFilterSystemApp = null, + Object? isFilterNonInternetApp = null, }) { return _then(_value.copyWith( enable: null == enable @@ -589,6 +638,10 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl> ? _value.isFilterSystemApp : isFilterSystemApp // ignore: cast_nullable_to_non_nullable as bool, + isFilterNonInternetApp: null == isFilterNonInternetApp + ? _value.isFilterNonInternetApp + : isFilterNonInternetApp // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -607,7 +660,8 @@ abstract class _$$AccessControlImplCopyWith<$Res> List acceptList, List rejectList, AccessSortType sort, - bool isFilterSystemApp}); + bool isFilterSystemApp, + bool isFilterNonInternetApp}); } /// @nodoc @@ -629,6 +683,7 @@ class __$$AccessControlImplCopyWithImpl<$Res> Object? rejectList = null, Object? sort = null, Object? isFilterSystemApp = null, + Object? isFilterNonInternetApp = null, }) { return _then(_$AccessControlImpl( enable: null == enable @@ -655,6 +710,10 @@ class __$$AccessControlImplCopyWithImpl<$Res> ? _value.isFilterSystemApp : isFilterSystemApp // ignore: cast_nullable_to_non_nullable as bool, + isFilterNonInternetApp: null == isFilterNonInternetApp + ? _value.isFilterNonInternetApp + : isFilterNonInternetApp // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -668,7 +727,8 @@ class _$AccessControlImpl implements _AccessControl { final List acceptList = const [], final List rejectList = const [], this.sort = AccessSortType.none, - this.isFilterSystemApp = true}) + this.isFilterSystemApp = true, + this.isFilterNonInternetApp = true}) : _acceptList = acceptList, _rejectList = rejectList; @@ -705,10 +765,13 @@ class _$AccessControlImpl implements _AccessControl { @override @JsonKey() final bool isFilterSystemApp; + @override + @JsonKey() + final bool isFilterNonInternetApp; @override String toString() { - return 'AccessControl(enable: $enable, mode: $mode, acceptList: $acceptList, rejectList: $rejectList, sort: $sort, isFilterSystemApp: $isFilterSystemApp)'; + return 'AccessControl(enable: $enable, mode: $mode, acceptList: $acceptList, rejectList: $rejectList, sort: $sort, isFilterSystemApp: $isFilterSystemApp, isFilterNonInternetApp: $isFilterNonInternetApp)'; } @override @@ -724,7 +787,9 @@ class _$AccessControlImpl implements _AccessControl { .equals(other._rejectList, _rejectList) && (identical(other.sort, sort) || other.sort == sort) && (identical(other.isFilterSystemApp, isFilterSystemApp) || - other.isFilterSystemApp == isFilterSystemApp)); + other.isFilterSystemApp == isFilterSystemApp) && + (identical(other.isFilterNonInternetApp, isFilterNonInternetApp) || + other.isFilterNonInternetApp == isFilterNonInternetApp)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -736,7 +801,8 @@ class _$AccessControlImpl implements _AccessControl { const DeepCollectionEquality().hash(_acceptList), const DeepCollectionEquality().hash(_rejectList), sort, - isFilterSystemApp); + isFilterSystemApp, + isFilterNonInternetApp); /// Create a copy of AccessControl /// with the given fields replaced by the non-null parameter values. @@ -761,7 +827,8 @@ abstract class _AccessControl implements AccessControl { final List acceptList, final List rejectList, final AccessSortType sort, - final bool isFilterSystemApp}) = _$AccessControlImpl; + final bool isFilterSystemApp, + final bool isFilterNonInternetApp}) = _$AccessControlImpl; factory _AccessControl.fromJson(Map json) = _$AccessControlImpl.fromJson; @@ -778,6 +845,8 @@ abstract class _AccessControl implements AccessControl { AccessSortType get sort; @override bool get isFilterSystemApp; + @override + bool get isFilterNonInternetApp; /// Create a copy of AccessControl /// with the given fields replaced by the non-null parameter values. @@ -1717,6 +1786,170 @@ abstract class _ProxiesStyle implements ProxiesStyle { throw _privateConstructorUsedError; } +TextScale _$TextScaleFromJson(Map json) { + return _TextScale.fromJson(json); +} + +/// @nodoc +mixin _$TextScale { + dynamic get enable => throw _privateConstructorUsedError; + dynamic get scale => throw _privateConstructorUsedError; + + /// Serializes this TextScale to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TextScale + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TextScaleCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TextScaleCopyWith<$Res> { + factory $TextScaleCopyWith(TextScale value, $Res Function(TextScale) then) = + _$TextScaleCopyWithImpl<$Res, TextScale>; + @useResult + $Res call({dynamic enable, dynamic scale}); +} + +/// @nodoc +class _$TextScaleCopyWithImpl<$Res, $Val extends TextScale> + implements $TextScaleCopyWith<$Res> { + _$TextScaleCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TextScale + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enable = freezed, + Object? scale = freezed, + }) { + return _then(_value.copyWith( + enable: freezed == enable + ? _value.enable + : enable // ignore: cast_nullable_to_non_nullable + as dynamic, + scale: freezed == scale + ? _value.scale + : scale // ignore: cast_nullable_to_non_nullable + as dynamic, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TextScaleImplCopyWith<$Res> + implements $TextScaleCopyWith<$Res> { + factory _$$TextScaleImplCopyWith( + _$TextScaleImpl value, $Res Function(_$TextScaleImpl) then) = + __$$TextScaleImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({dynamic enable, dynamic scale}); +} + +/// @nodoc +class __$$TextScaleImplCopyWithImpl<$Res> + extends _$TextScaleCopyWithImpl<$Res, _$TextScaleImpl> + implements _$$TextScaleImplCopyWith<$Res> { + __$$TextScaleImplCopyWithImpl( + _$TextScaleImpl _value, $Res Function(_$TextScaleImpl) _then) + : super(_value, _then); + + /// Create a copy of TextScale + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enable = freezed, + Object? scale = freezed, + }) { + return _then(_$TextScaleImpl( + enable: freezed == enable ? _value.enable! : enable, + scale: freezed == scale ? _value.scale! : scale, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TextScaleImpl implements _TextScale { + const _$TextScaleImpl({this.enable = false, this.scale = 1.0}); + + factory _$TextScaleImpl.fromJson(Map json) => + _$$TextScaleImplFromJson(json); + + @override + @JsonKey() + final dynamic enable; + @override + @JsonKey() + final dynamic scale; + + @override + String toString() { + return 'TextScale(enable: $enable, scale: $scale)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TextScaleImpl && + const DeepCollectionEquality().equals(other.enable, enable) && + const DeepCollectionEquality().equals(other.scale, scale)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(enable), + const DeepCollectionEquality().hash(scale)); + + /// Create a copy of TextScale + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TextScaleImplCopyWith<_$TextScaleImpl> get copyWith => + __$$TextScaleImplCopyWithImpl<_$TextScaleImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TextScaleImplToJson( + this, + ); + } +} + +abstract class _TextScale implements TextScale { + const factory _TextScale({final dynamic enable, final dynamic scale}) = + _$TextScaleImpl; + + factory _TextScale.fromJson(Map json) = + _$TextScaleImpl.fromJson; + + @override + dynamic get enable; + @override + dynamic get scale; + + /// Create a copy of TextScale + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TextScaleImplCopyWith<_$TextScaleImpl> get copyWith => + throw _privateConstructorUsedError; +} + ThemeProps _$ThemePropsFromJson(Map json) { return _ThemeProps.fromJson(json); } @@ -1728,6 +1961,7 @@ mixin _$ThemeProps { ThemeMode get themeMode => throw _privateConstructorUsedError; DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; bool get pureBlack => throw _privateConstructorUsedError; + TextScale get textScale => throw _privateConstructorUsedError; /// Serializes this ThemeProps to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1750,7 +1984,10 @@ abstract class $ThemePropsCopyWith<$Res> { List primaryColors, ThemeMode themeMode, DynamicSchemeVariant schemeVariant, - bool pureBlack}); + bool pureBlack, + TextScale textScale}); + + $TextScaleCopyWith<$Res> get textScale; } /// @nodoc @@ -1773,6 +2010,7 @@ class _$ThemePropsCopyWithImpl<$Res, $Val extends ThemeProps> Object? themeMode = null, Object? schemeVariant = null, Object? pureBlack = null, + Object? textScale = null, }) { return _then(_value.copyWith( primaryColor: freezed == primaryColor @@ -1795,8 +2033,22 @@ class _$ThemePropsCopyWithImpl<$Res, $Val extends ThemeProps> ? _value.pureBlack : pureBlack // ignore: cast_nullable_to_non_nullable as bool, + textScale: null == textScale + ? _value.textScale + : textScale // ignore: cast_nullable_to_non_nullable + as TextScale, ) as $Val); } + + /// Create a copy of ThemeProps + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $TextScaleCopyWith<$Res> get textScale { + return $TextScaleCopyWith<$Res>(_value.textScale, (value) { + return _then(_value.copyWith(textScale: value) as $Val); + }); + } } /// @nodoc @@ -1812,7 +2064,11 @@ abstract class _$$ThemePropsImplCopyWith<$Res> List primaryColors, ThemeMode themeMode, DynamicSchemeVariant schemeVariant, - bool pureBlack}); + bool pureBlack, + TextScale textScale}); + + @override + $TextScaleCopyWith<$Res> get textScale; } /// @nodoc @@ -1833,6 +2089,7 @@ class __$$ThemePropsImplCopyWithImpl<$Res> Object? themeMode = null, Object? schemeVariant = null, Object? pureBlack = null, + Object? textScale = null, }) { return _then(_$ThemePropsImpl( primaryColor: freezed == primaryColor @@ -1855,6 +2112,10 @@ class __$$ThemePropsImplCopyWithImpl<$Res> ? _value.pureBlack : pureBlack // ignore: cast_nullable_to_non_nullable as bool, + textScale: null == textScale + ? _value.textScale + : textScale // ignore: cast_nullable_to_non_nullable + as TextScale, )); } } @@ -1863,18 +2124,18 @@ class __$$ThemePropsImplCopyWithImpl<$Res> @JsonSerializable() class _$ThemePropsImpl implements _ThemeProps { const _$ThemePropsImpl( - {this.primaryColor = defaultPrimaryColor, + {this.primaryColor, final List primaryColors = defaultPrimaryColors, this.themeMode = ThemeMode.dark, - this.schemeVariant = DynamicSchemeVariant.tonalSpot, - this.pureBlack = false}) + this.schemeVariant = DynamicSchemeVariant.content, + this.pureBlack = false, + this.textScale = const TextScale()}) : _primaryColors = primaryColors; factory _$ThemePropsImpl.fromJson(Map json) => _$$ThemePropsImplFromJson(json); @override - @JsonKey() final int? primaryColor; final List _primaryColors; @override @@ -1894,10 +2155,13 @@ class _$ThemePropsImpl implements _ThemeProps { @override @JsonKey() final bool pureBlack; + @override + @JsonKey() + final TextScale textScale; @override String toString() { - return 'ThemeProps(primaryColor: $primaryColor, primaryColors: $primaryColors, themeMode: $themeMode, schemeVariant: $schemeVariant, pureBlack: $pureBlack)'; + return 'ThemeProps(primaryColor: $primaryColor, primaryColors: $primaryColors, themeMode: $themeMode, schemeVariant: $schemeVariant, pureBlack: $pureBlack, textScale: $textScale)'; } @override @@ -1914,7 +2178,9 @@ class _$ThemePropsImpl implements _ThemeProps { (identical(other.schemeVariant, schemeVariant) || other.schemeVariant == schemeVariant) && (identical(other.pureBlack, pureBlack) || - other.pureBlack == pureBlack)); + other.pureBlack == pureBlack) && + (identical(other.textScale, textScale) || + other.textScale == textScale)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -1925,7 +2191,8 @@ class _$ThemePropsImpl implements _ThemeProps { const DeepCollectionEquality().hash(_primaryColors), themeMode, schemeVariant, - pureBlack); + pureBlack, + textScale); /// Create a copy of ThemeProps /// with the given fields replaced by the non-null parameter values. @@ -1949,7 +2216,8 @@ abstract class _ThemeProps implements ThemeProps { final List primaryColors, final ThemeMode themeMode, final DynamicSchemeVariant schemeVariant, - final bool pureBlack}) = _$ThemePropsImpl; + final bool pureBlack, + final TextScale textScale}) = _$ThemePropsImpl; factory _ThemeProps.fromJson(Map json) = _$ThemePropsImpl.fromJson; @@ -1964,6 +2232,8 @@ abstract class _ThemeProps implements ThemeProps { DynamicSchemeVariant get schemeVariant; @override bool get pureBlack; + @override + TextScale get textScale; /// Create a copy of ThemeProps /// with the given fields replaced by the non-null parameter values. @@ -1988,6 +2258,7 @@ mixin _$Config { DAV? get dav => throw _privateConstructorUsedError; NetworkProps get networkProps => throw _privateConstructorUsedError; VpnProps get vpnProps => throw _privateConstructorUsedError; + @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps get themeProps => throw _privateConstructorUsedError; ProxiesStyle get proxiesStyle => throw _privateConstructorUsedError; WindowProps get windowProps => throw _privateConstructorUsedError; @@ -2017,7 +2288,7 @@ abstract class $ConfigCopyWith<$Res> { DAV? dav, NetworkProps networkProps, VpnProps vpnProps, - ThemeProps themeProps, + @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps themeProps, ProxiesStyle proxiesStyle, WindowProps windowProps, ClashConfig patchClashConfig}); @@ -2214,7 +2485,7 @@ abstract class _$$ConfigImplCopyWith<$Res> implements $ConfigCopyWith<$Res> { DAV? dav, NetworkProps networkProps, VpnProps vpnProps, - ThemeProps themeProps, + @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps themeProps, ProxiesStyle proxiesStyle, WindowProps windowProps, ClashConfig patchClashConfig}); @@ -2329,7 +2600,7 @@ class _$ConfigImpl implements _Config { this.dav, this.networkProps = defaultNetworkProps, this.vpnProps = defaultVpnProps, - this.themeProps = defaultThemeProps, + @JsonKey(fromJson: ThemeProps.safeFromJson) required this.themeProps, this.proxiesStyle = defaultProxiesStyle, this.windowProps = defaultWindowProps, this.patchClashConfig = defaultClashConfig}) @@ -2374,7 +2645,7 @@ class _$ConfigImpl implements _Config { @JsonKey() final VpnProps vpnProps; @override - @JsonKey() + @JsonKey(fromJson: ThemeProps.safeFromJson) final ThemeProps themeProps; @override @JsonKey() @@ -2464,7 +2735,8 @@ abstract class _Config implements Config { final DAV? dav, final NetworkProps networkProps, final VpnProps vpnProps, - final ThemeProps themeProps, + @JsonKey(fromJson: ThemeProps.safeFromJson) + required final ThemeProps themeProps, final ProxiesStyle proxiesStyle, final WindowProps windowProps, final ClashConfig patchClashConfig}) = _$ConfigImpl; @@ -2489,6 +2761,7 @@ abstract class _Config implements Config { @override VpnProps get vpnProps; @override + @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps get themeProps; @override ProxiesStyle get proxiesStyle; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 345d6b9..741a90c 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -17,7 +17,7 @@ _$AppSettingPropsImpl _$$AppSettingPropsImplFromJson( autoLaunch: json['autoLaunch'] as bool? ?? false, silentLaunch: json['silentLaunch'] as bool? ?? false, autoRun: json['autoRun'] as bool? ?? false, - openLogs: json['openLogs'] as bool? ?? true, + openLogs: json['openLogs'] as bool? ?? false, closeConnections: json['closeConnections'] as bool? ?? true, testUrl: json['testUrl'] as String? ?? defaultTestUrl, isAnimateToPage: json['isAnimateToPage'] as bool? ?? true, @@ -26,6 +26,10 @@ _$AppSettingPropsImpl _$$AppSettingPropsImplFromJson( disclaimerAccepted: json['disclaimerAccepted'] as bool? ?? false, minimizeOnExit: json['minimizeOnExit'] as bool? ?? true, hidden: json['hidden'] as bool? ?? false, + developerMode: json['developerMode'] as bool? ?? false, + recoveryStrategy: $enumDecodeNullable( + _$RecoveryStrategyEnumMap, json['recoveryStrategy']) ?? + RecoveryStrategy.compatible, ); Map _$$AppSettingPropsImplToJson( @@ -48,14 +52,23 @@ Map _$$AppSettingPropsImplToJson( 'disclaimerAccepted': instance.disclaimerAccepted, 'minimizeOnExit': instance.minimizeOnExit, 'hidden': instance.hidden, + 'developerMode': instance.developerMode, + 'recoveryStrategy': _$RecoveryStrategyEnumMap[instance.recoveryStrategy]!, }; +const _$RecoveryStrategyEnumMap = { + RecoveryStrategy.compatible: 'compatible', + RecoveryStrategy.override: 'override', +}; + const _$DashboardWidgetEnumMap = { DashboardWidget.networkSpeed: 'networkSpeed', + DashboardWidget.outboundModeV2: 'outboundModeV2', DashboardWidget.outboundMode: 'outboundMode', DashboardWidget.trafficUsage: 'trafficUsage', DashboardWidget.networkDetection: 'networkDetection', DashboardWidget.tunButton: 'tunButton', + DashboardWidget.vpnButton: 'vpnButton', DashboardWidget.systemProxyButton: 'systemProxyButton', DashboardWidget.intranetIp: 'intranetIp', DashboardWidget.memoryInfo: 'memoryInfo', @@ -77,6 +90,7 @@ _$AccessControlImpl _$$AccessControlImplFromJson(Map json) => sort: $enumDecodeNullable(_$AccessSortTypeEnumMap, json['sort']) ?? AccessSortType.none, isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true, + isFilterNonInternetApp: json['isFilterNonInternetApp'] as bool? ?? true, ); Map _$$AccessControlImplToJson(_$AccessControlImpl instance) => @@ -87,6 +101,7 @@ Map _$$AccessControlImplToJson(_$AccessControlImpl instance) => 'rejectList': instance.rejectList, 'sort': _$AccessSortTypeEnumMap[instance.sort]!, 'isFilterSystemApp': instance.isFilterSystemApp, + 'isFilterNonInternetApp': instance.isFilterNonInternetApp, }; const _$AccessControlModeEnumMap = { @@ -219,10 +234,21 @@ const _$ProxyCardTypeEnumMap = { ProxyCardType.min: 'min', }; +_$TextScaleImpl _$$TextScaleImplFromJson(Map json) => + _$TextScaleImpl( + enable: json['enable'] ?? false, + scale: json['scale'] ?? 1.0, + ); + +Map _$$TextScaleImplToJson(_$TextScaleImpl instance) => + { + 'enable': instance.enable, + 'scale': instance.scale, + }; + _$ThemePropsImpl _$$ThemePropsImplFromJson(Map json) => _$ThemePropsImpl( - primaryColor: - (json['primaryColor'] as num?)?.toInt() ?? defaultPrimaryColor, + primaryColor: (json['primaryColor'] as num?)?.toInt(), primaryColors: (json['primaryColors'] as List?) ?.map((e) => (e as num).toInt()) .toList() ?? @@ -231,8 +257,11 @@ _$ThemePropsImpl _$$ThemePropsImplFromJson(Map json) => ThemeMode.dark, schemeVariant: $enumDecodeNullable( _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? - DynamicSchemeVariant.tonalSpot, + DynamicSchemeVariant.content, pureBlack: json['pureBlack'] as bool? ?? false, + textScale: json['textScale'] == null + ? const TextScale() + : TextScale.fromJson(json['textScale'] as Map), ); Map _$$ThemePropsImplToJson(_$ThemePropsImpl instance) => @@ -242,6 +271,7 @@ Map _$$ThemePropsImplToJson(_$ThemePropsImpl instance) => 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'pureBlack': instance.pureBlack, + 'textScale': instance.textScale, }; const _$ThemeModeEnumMap = { @@ -287,9 +317,8 @@ _$ConfigImpl _$$ConfigImplFromJson(Map json) => _$ConfigImpl( vpnProps: json['vpnProps'] == null ? defaultVpnProps : VpnProps.fromJson(json['vpnProps'] as Map?), - themeProps: json['themeProps'] == null - ? defaultThemeProps - : ThemeProps.fromJson(json['themeProps'] as Map?), + themeProps: + ThemeProps.safeFromJson(json['themeProps'] as Map?), proxiesStyle: json['proxiesStyle'] == null ? defaultProxiesStyle : ProxiesStyle.fromJson( diff --git a/lib/models/generated/core.g.dart b/lib/models/generated/core.g.dart index b951e47..9bc12e5 100644 --- a/lib/models/generated/core.g.dart +++ b/lib/models/generated/core.g.dart @@ -345,6 +345,7 @@ const _$ActionMethodEnumMap = { ActionMethod.getCountryCode: 'getCountryCode', ActionMethod.getMemory: 'getMemory', ActionMethod.getProfile: 'getProfile', + ActionMethod.crash: 'crash', ActionMethod.setFdMap: 'setFdMap', ActionMethod.setProcessMap: 'setProcessMap', ActionMethod.setState: 'setState', diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 9aff17d..34db08f 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -327,6 +327,193 @@ abstract class _VM3 implements VM3 { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$VM4 { + A get a => throw _privateConstructorUsedError; + B get b => throw _privateConstructorUsedError; + C get c => throw _privateConstructorUsedError; + D get d => throw _privateConstructorUsedError; + + /// Create a copy of VM4 + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VM4CopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VM4CopyWith { + factory $VM4CopyWith( + VM4 value, $Res Function(VM4) then) = + _$VM4CopyWithImpl>; + @useResult + $Res call({A a, B b, C c, D d}); +} + +/// @nodoc +class _$VM4CopyWithImpl> + implements $VM4CopyWith { + _$VM4CopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VM4 + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? a = freezed, + Object? b = freezed, + Object? c = freezed, + Object? d = freezed, + }) { + return _then(_value.copyWith( + a: freezed == a + ? _value.a + : a // ignore: cast_nullable_to_non_nullable + as A, + b: freezed == b + ? _value.b + : b // ignore: cast_nullable_to_non_nullable + as B, + c: freezed == c + ? _value.c + : c // ignore: cast_nullable_to_non_nullable + as C, + d: freezed == d + ? _value.d + : d // ignore: cast_nullable_to_non_nullable + as D, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$VM4ImplCopyWith + implements $VM4CopyWith { + factory _$$VM4ImplCopyWith(_$VM4Impl value, + $Res Function(_$VM4Impl) then) = + __$$VM4ImplCopyWithImpl; + @override + @useResult + $Res call({A a, B b, C c, D d}); +} + +/// @nodoc +class __$$VM4ImplCopyWithImpl + extends _$VM4CopyWithImpl> + implements _$$VM4ImplCopyWith { + __$$VM4ImplCopyWithImpl( + _$VM4Impl _value, $Res Function(_$VM4Impl) _then) + : super(_value, _then); + + /// Create a copy of VM4 + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? a = freezed, + Object? b = freezed, + Object? c = freezed, + Object? d = freezed, + }) { + return _then(_$VM4Impl( + a: freezed == a + ? _value.a + : a // ignore: cast_nullable_to_non_nullable + as A, + b: freezed == b + ? _value.b + : b // ignore: cast_nullable_to_non_nullable + as B, + c: freezed == c + ? _value.c + : c // ignore: cast_nullable_to_non_nullable + as C, + d: freezed == d + ? _value.d + : d // ignore: cast_nullable_to_non_nullable + as D, + )); + } +} + +/// @nodoc + +class _$VM4Impl implements _VM4 { + const _$VM4Impl( + {required this.a, required this.b, required this.c, required this.d}); + + @override + final A a; + @override + final B b; + @override + final C c; + @override + final D d; + + @override + String toString() { + return 'VM4<$A, $B, $C, $D>(a: $a, b: $b, c: $c, d: $d)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VM4Impl && + const DeepCollectionEquality().equals(other.a, a) && + const DeepCollectionEquality().equals(other.b, b) && + const DeepCollectionEquality().equals(other.c, c) && + const DeepCollectionEquality().equals(other.d, d)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(a), + const DeepCollectionEquality().hash(b), + const DeepCollectionEquality().hash(c), + const DeepCollectionEquality().hash(d)); + + /// Create a copy of VM4 + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VM4ImplCopyWith> get copyWith => + __$$VM4ImplCopyWithImpl>( + this, _$identity); +} + +abstract class _VM4 implements VM4 { + const factory _VM4( + {required final A a, + required final B b, + required final C c, + required final D d}) = _$VM4Impl; + + @override + A get a; + @override + B get b; + @override + C get c; + @override + D get d; + + /// Create a copy of VM4 + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VM4ImplCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$StartButtonSelectorState { bool get isInit => throw _privateConstructorUsedError; @@ -3335,6 +3522,7 @@ mixin _$ClashConfigState { bool get overrideDns => throw _privateConstructorUsedError; ClashConfig get clashConfig => throw _privateConstructorUsedError; OverrideData get overrideData => throw _privateConstructorUsedError; + RouteMode get routeMode => throw _privateConstructorUsedError; /// Create a copy of ClashConfigState /// with the given fields replaced by the non-null parameter values. @@ -3350,7 +3538,10 @@ abstract class $ClashConfigStateCopyWith<$Res> { _$ClashConfigStateCopyWithImpl<$Res, ClashConfigState>; @useResult $Res call( - {bool overrideDns, ClashConfig clashConfig, OverrideData overrideData}); + {bool overrideDns, + ClashConfig clashConfig, + OverrideData overrideData, + RouteMode routeMode}); $ClashConfigCopyWith<$Res> get clashConfig; $OverrideDataCopyWith<$Res> get overrideData; @@ -3374,6 +3565,7 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> Object? overrideDns = null, Object? clashConfig = null, Object? overrideData = null, + Object? routeMode = null, }) { return _then(_value.copyWith( overrideDns: null == overrideDns @@ -3388,6 +3580,10 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> ? _value.overrideData : overrideData // ignore: cast_nullable_to_non_nullable as OverrideData, + routeMode: null == routeMode + ? _value.routeMode + : routeMode // ignore: cast_nullable_to_non_nullable + as RouteMode, ) as $Val); } @@ -3421,7 +3617,10 @@ abstract class _$$ClashConfigStateImplCopyWith<$Res> @override @useResult $Res call( - {bool overrideDns, ClashConfig clashConfig, OverrideData overrideData}); + {bool overrideDns, + ClashConfig clashConfig, + OverrideData overrideData, + RouteMode routeMode}); @override $ClashConfigCopyWith<$Res> get clashConfig; @@ -3445,6 +3644,7 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> Object? overrideDns = null, Object? clashConfig = null, Object? overrideData = null, + Object? routeMode = null, }) { return _then(_$ClashConfigStateImpl( overrideDns: null == overrideDns @@ -3459,6 +3659,10 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> ? _value.overrideData : overrideData // ignore: cast_nullable_to_non_nullable as OverrideData, + routeMode: null == routeMode + ? _value.routeMode + : routeMode // ignore: cast_nullable_to_non_nullable + as RouteMode, )); } } @@ -3469,7 +3673,8 @@ class _$ClashConfigStateImpl implements _ClashConfigState { const _$ClashConfigStateImpl( {required this.overrideDns, required this.clashConfig, - required this.overrideData}); + required this.overrideData, + required this.routeMode}); @override final bool overrideDns; @@ -3477,10 +3682,12 @@ class _$ClashConfigStateImpl implements _ClashConfigState { final ClashConfig clashConfig; @override final OverrideData overrideData; + @override + final RouteMode routeMode; @override String toString() { - return 'ClashConfigState(overrideDns: $overrideDns, clashConfig: $clashConfig, overrideData: $overrideData)'; + return 'ClashConfigState(overrideDns: $overrideDns, clashConfig: $clashConfig, overrideData: $overrideData, routeMode: $routeMode)'; } @override @@ -3493,12 +3700,14 @@ class _$ClashConfigStateImpl implements _ClashConfigState { (identical(other.clashConfig, clashConfig) || other.clashConfig == clashConfig) && (identical(other.overrideData, overrideData) || - other.overrideData == overrideData)); + other.overrideData == overrideData) && + (identical(other.routeMode, routeMode) || + other.routeMode == routeMode)); } @override - int get hashCode => - Object.hash(runtimeType, overrideDns, clashConfig, overrideData); + int get hashCode => Object.hash( + runtimeType, overrideDns, clashConfig, overrideData, routeMode); /// Create a copy of ClashConfigState /// with the given fields replaced by the non-null parameter values. @@ -3514,7 +3723,8 @@ abstract class _ClashConfigState implements ClashConfigState { const factory _ClashConfigState( {required final bool overrideDns, required final ClashConfig clashConfig, - required final OverrideData overrideData}) = _$ClashConfigStateImpl; + required final OverrideData overrideData, + required final RouteMode routeMode}) = _$ClashConfigStateImpl; @override bool get overrideDns; @@ -3522,6 +3732,8 @@ abstract class _ClashConfigState implements ClashConfigState { ClashConfig get clashConfig; @override OverrideData get overrideData; + @override + RouteMode get routeMode; /// Create a copy of ClashConfigState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 2087c0d..11d1d83 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -24,6 +24,17 @@ class VM3 with _$VM3 { }) = _VM3; } +@freezed +class VM4 with _$VM4 { + const factory VM4({ + required A a, + required B b, + required C c, + required D d, + }) = _VM4; +} + + @freezed class StartButtonSelectorState with _$StartButtonSelectorState { const factory StartButtonSelectorState({ @@ -146,12 +157,21 @@ class PackageListSelectorState with _$PackageListSelectorState { } extension PackageListSelectorStateExt on PackageListSelectorState { - List getList(List selectedList) { + List get list { final isFilterSystemApp = accessControl.isFilterSystemApp; - final sort = accessControl.sort; + final isFilterNonInternetApp = accessControl.isFilterNonInternetApp; return packages - .where((item) => isFilterSystemApp ? item.isSystem == false : true) - .sorted( + .where( + (item) => + (isFilterSystemApp ? item.system == false : true) && + (isFilterNonInternetApp ? item.internet == true : true), + ) + .toList(); + } + + List getSortList(List selectedList) { + final sort = accessControl.sort; + return list.sorted( (a, b) { return switch (sort) { AccessSortType.none => 0, @@ -208,6 +228,7 @@ class ClashConfigState with _$ClashConfigState { required bool overrideDns, required ClashConfig clashConfig, required OverrideData overrideData, + required RouteMode routeMode, }) = _ClashConfigState; } diff --git a/lib/pages/editor.dart b/lib/pages/editor.dart index 88e340b..bd25441 100644 --- a/lib/pages/editor.dart +++ b/lib/pages/editor.dart @@ -127,6 +127,7 @@ class _EditorPageState extends ConsumerState { ); }, popup: CommonPopupMenu( + minWidth: 180, items: [ PopupMenuItemData( icon: Icons.search, @@ -189,7 +190,7 @@ class _EditorPageState extends ConsumerState { shortcutsActivatorsBuilder: DefaultCodeShortcutsActivatorsBuilder(), controller: _controller, style: CodeEditorStyle( - fontSize: 14, + fontSize: 14.ap, fontFamily: FontFamily.jetBrainsMono.value, codeTheme: CodeHighlightTheme( languages: { diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ad4452f..bb11138 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -129,6 +129,16 @@ class _HomePageViewState extends ConsumerState<_HomePageView> { controller: _pageController, physics: const NeverScrollableScrollPhysics(), itemCount: navigationItems.length, + // onPageChanged: (index) { + // debouncer.call(DebounceTag.pageChange, () { + // WidgetsBinding.instance.addPostFrameCallback((_) { + // if (_pageIndex != index) { + // final pageLabel = navigationItems[index].label; + // _toPage(pageLabel, true); + // } + // }); + // }); + // }, itemBuilder: (_, index) { final navigationItem = navigationItems[index]; return KeepScope( @@ -180,43 +190,46 @@ class CommonNavigationBar extends ConsumerWidget { child: Column( children: [ Expanded( - child: SingleChildScrollView( - child: IntrinsicHeight( - child: NavigationRail( - backgroundColor: context.colorScheme.surfaceContainer, - selectedIconTheme: IconThemeData( - color: context.colorScheme.onSurfaceVariant, - ), - unselectedIconTheme: IconThemeData( - color: context.colorScheme.onSurfaceVariant, - ), - 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: ScrollConfiguration( + behavior: HiddenBarScrollBehavior(), + child: SingleChildScrollView( + child: IntrinsicHeight( + child: NavigationRail( + backgroundColor: context.colorScheme.surfaceContainer, + selectedIconTheme: IconThemeData( + color: context.colorScheme.onSurfaceVariant, + ), + unselectedIconTheme: IconThemeData( + color: context.colorScheme.onSurfaceVariant, + ), + selectedLabelTextStyle: + context.textTheme.labelLarge!.copyWith( + color: context.colorScheme.onSurface, + ), + unselectedLabelTextStyle: + context.textTheme.labelLarge!.copyWith( + color: context.colorScheme.onSurface, + ), + destinations: navigationItems + .map( + (e) => NavigationRailDestination( + icon: e.icon, + label: Text( + Intl.message(e.label.name), + ), ), - ), - ) - .toList(), - onDestinationSelected: (index) { - globalState.appController - .toPage(navigationItems[index].label); - }, - extended: false, - selectedIndex: currentIndex, - labelType: showLabel - ? NavigationRailLabelType.all - : NavigationRailLabelType.none, + ) + .toList(), + onDestinationSelected: (index) { + globalState.appController + .toPage(navigationItems[index].label); + }, + extended: false, + selectedIndex: currentIndex, + labelType: showLabel + ? NavigationRailLabelType.all + : NavigationRailLabelType.none, + ), ), ), ), diff --git a/lib/providers/app.dart b/lib/providers/app.dart index 6a5ca5f..89bf3ae 100644 --- a/lib/providers/app.dart +++ b/lib/providers/app.dart @@ -252,21 +252,6 @@ class CurrentPageLabel extends _$CurrentPageLabel } } -@riverpod -class AppSchemes extends _$AppSchemes with AutoDisposeNotifierMixin { - @override - ColorSchemes build() { - return globalState.appState.colorSchemes; - } - - @override - onUpdate(value) { - globalState.appState = globalState.appState.copyWith( - colorSchemes: value, - ); - } -} - @riverpod class SortNum extends _$SortNum with AutoDisposeNotifierMixin { @override diff --git a/lib/providers/generated/app.g.dart b/lib/providers/generated/app.g.dart index f4e653c..14524c1 100644 --- a/lib/providers/generated/app.g.dart +++ b/lib/providers/generated/app.g.dart @@ -247,21 +247,6 @@ final currentPageLabelProvider = ); typedef _$CurrentPageLabel = AutoDisposeNotifier; -String _$appSchemesHash() => r'748f48f23539a879a92f318a21e1266b1df56aae'; - -/// See also [AppSchemes]. -@ProviderFor(AppSchemes) -final appSchemesProvider = - AutoDisposeNotifierProvider.internal( - AppSchemes.new, - name: r'appSchemesProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$appSchemesHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$AppSchemes = AutoDisposeNotifier; String _$sortNumHash() => r'0f85ebbc77124020eaccf988c6ac9d86a7f34d7e'; /// See also [SortNum]. diff --git a/lib/providers/generated/state.g.dart b/lib/providers/generated/state.g.dart index 7ae3236..ab3f932 100644 --- a/lib/providers/generated/state.g.dart +++ b/lib/providers/generated/state.g.dart @@ -78,7 +78,7 @@ final coreStateProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef CoreStateRef = AutoDisposeProviderRef; -String _$clashConfigStateHash() => r'848f6b2f734d99fb11ec05f73d614be415e9658f'; +String _$clashConfigStateHash() => r'fbbcd7221b0b9b18db523e59c9021e8e56e119ca'; /// See also [clashConfigState]. @ProviderFor(clashConfigState) @@ -1765,7 +1765,23 @@ class _GetProfileOverrideDataProviderElement String get profileId => (origin as GetProfileOverrideDataProvider).profileId; } -String _$genColorSchemeHash() => r'a27ccae9b5c11d47cd46804f42f8e9dc7946a6c2'; +String _$layoutChangeHash() => r'f25182e1dfaf3c70000404d7635bb1e1db09efbb'; + +/// See also [layoutChange]. +@ProviderFor(layoutChange) +final layoutChangeProvider = AutoDisposeProvider.internal( + layoutChange, + name: r'layoutChangeProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$layoutChangeHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef LayoutChangeRef = AutoDisposeProviderRef; +String _$genColorSchemeHash() => r'b18f15c938a8132ee4ed02cdfc02f3b9f01724e2'; /// See also [genColorScheme]. @ProviderFor(genColorScheme) @@ -1780,12 +1796,12 @@ class GenColorSchemeFamily extends Family { GenColorSchemeProvider call( Brightness brightness, { Color? color, - bool isOverride = false, + bool ignoreConfig = false, }) { return GenColorSchemeProvider( brightness, color: color, - isOverride: isOverride, + ignoreConfig: ignoreConfig, ); } @@ -1796,7 +1812,7 @@ class GenColorSchemeFamily extends Family { return call( provider.brightness, color: provider.color, - isOverride: provider.isOverride, + ignoreConfig: provider.ignoreConfig, ); } @@ -1821,13 +1837,13 @@ class GenColorSchemeProvider extends AutoDisposeProvider { GenColorSchemeProvider( Brightness brightness, { Color? color, - bool isOverride = false, + bool ignoreConfig = false, }) : this._internal( (ref) => genColorScheme( ref as GenColorSchemeRef, brightness, color: color, - isOverride: isOverride, + ignoreConfig: ignoreConfig, ), from: genColorSchemeProvider, name: r'genColorSchemeProvider', @@ -1840,7 +1856,7 @@ class GenColorSchemeProvider extends AutoDisposeProvider { GenColorSchemeFamily._allTransitiveDependencies, brightness: brightness, color: color, - isOverride: isOverride, + ignoreConfig: ignoreConfig, ); GenColorSchemeProvider._internal( @@ -1852,12 +1868,12 @@ class GenColorSchemeProvider extends AutoDisposeProvider { required super.from, required this.brightness, required this.color, - required this.isOverride, + required this.ignoreConfig, }) : super.internal(); final Brightness brightness; final Color? color; - final bool isOverride; + final bool ignoreConfig; @override Override overrideWith( @@ -1874,7 +1890,7 @@ class GenColorSchemeProvider extends AutoDisposeProvider { debugGetCreateSourceHash: null, brightness: brightness, color: color, - isOverride: isOverride, + ignoreConfig: ignoreConfig, ), ); } @@ -1889,7 +1905,7 @@ class GenColorSchemeProvider extends AutoDisposeProvider { return other is GenColorSchemeProvider && other.brightness == brightness && other.color == color && - other.isOverride == isOverride; + other.ignoreConfig == ignoreConfig; } @override @@ -1897,7 +1913,7 @@ class GenColorSchemeProvider extends AutoDisposeProvider { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, brightness.hashCode); hash = _SystemHash.combine(hash, color.hashCode); - hash = _SystemHash.combine(hash, isOverride.hashCode); + hash = _SystemHash.combine(hash, ignoreConfig.hashCode); return _SystemHash.finish(hash); } @@ -1912,8 +1928,8 @@ mixin GenColorSchemeRef on AutoDisposeProviderRef { /// The parameter `color` of this provider. Color? get color; - /// The parameter `isOverride` of this provider. - bool get isOverride; + /// The parameter `ignoreConfig` of this provider. + bool get ignoreConfig; } class _GenColorSchemeProviderElement @@ -1925,7 +1941,7 @@ class _GenColorSchemeProviderElement @override Color? get color => (origin as GenColorSchemeProvider).color; @override - bool get isOverride => (origin as GenColorSchemeProvider).isOverride; + bool get ignoreConfig => (origin as GenColorSchemeProvider).ignoreConfig; } String _$profileOverrideStateHash() => diff --git a/lib/providers/state.dart b/lib/providers/state.dart index 26cb7ff..9359f3b 100644 --- a/lib/providers/state.dart +++ b/lib/providers/state.dart @@ -1,6 +1,8 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -73,13 +75,15 @@ CoreState coreState(Ref ref) { ClashConfigState clashConfigState(Ref ref) { final clashConfig = ref.watch(patchClashConfigProvider); final overrideDns = ref.watch(overrideDnsProvider); - final overrideData = ref.watch(currentProfileProvider.select( - (state) => state?.overrideData, - )); + final overrideData = + ref.watch(currentProfileProvider.select((state) => state?.overrideData)); + final routeMode = + ref.watch(networkSettingProvider.select((state) => state.routeMode)); return ClashConfigState( overrideDns: overrideDns, clashConfig: clashConfig, overrideData: overrideData ?? OverrideData(), + routeMode: routeMode, ); } @@ -506,12 +510,23 @@ OverrideData? getProfileOverrideData(Ref ref, String profileId) { ); } +@riverpod +VM2? layoutChange(Ref ref) { + final viewWidth = ref.watch(viewWidthProvider); + final textScale = + ref.watch(themeSettingProvider.select((state) => state.textScale)); + return VM2( + a: viewWidth, + b: textScale, + ); +} + @riverpod ColorScheme genColorScheme( Ref ref, Brightness brightness, { Color? color, - bool isOverride = false, + bool ignoreConfig = false, }) { final vm2 = ref.watch( themeSettingProvider.select( @@ -521,11 +536,17 @@ ColorScheme genColorScheme( ), ), ); - if (color == null && (isOverride == true || vm2.a == null)) { - final colorSchemes = ref.watch(appSchemesProvider); - return colorSchemes.getColorSchemeForBrightness( - brightness, - vm2.b, + if (color == null && (ignoreConfig == true || vm2.a == null)) { + // if (globalState.corePalette != null) { + // return globalState.corePalette!.toColorScheme(brightness: brightness); + // } + return ColorScheme.fromSeed( + seedColor: globalState.corePalette + ?.toColorScheme(brightness: brightness) + .primary ?? + globalState.accentColor, + brightness: brightness, + dynamicSchemeVariant: vm2.b, ); } return ColorScheme.fromSeed( diff --git a/lib/state.dart b/lib/state.dart index 929536f..6ca64eb 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -1,6 +1,6 @@ import 'dart:async'; - import 'package:animations/animations.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/theme.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -9,6 +9,7 @@ import 'package:fl_clash/plugins/service.dart'; import 'package:fl_clash/widgets/dialog.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; +import 'package:material_color_utilities/palettes/core_palette.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -20,17 +21,21 @@ typedef UpdateTasks = List; class GlobalState { static GlobalState? _instance; - Map cacheScrollPosition = {}; + Map cacheScrollPosition = {}; + Map> cacheHeightMap = {}; bool isService = false; Timer? timer; Timer? groupsUpdateTimer; late Config config; late AppState appState; bool isPre = true; + String? coreSHA256; late PackageInfo packageInfo; Function? updateCurrentDelayDebounce; late Measure measure; late CommonTheme theme; + late Color accentColor; + CorePalette? corePalette; DateTime? startTime; UpdateTasks tasks = []; final navigatorKey = GlobalKey(); @@ -50,14 +55,23 @@ class GlobalState { appState = AppState( version: version, viewSize: Size.zero, - requests: FixedList(1000), - logs: FixedList(1000), + requests: FixedList(maxLength), + logs: FixedList(maxLength), traffics: FixedList(30), totalTraffic: Traffic(), ); + await _initDynamicColor(); await init(); } + _initDynamicColor() async { + try { + corePalette = await DynamicColorPlugin.getCorePalette(); + accentColor = await DynamicColorPlugin.getAccentColor() ?? + Color(defaultPrimaryColor); + } catch (_) {} + } + init() async { packageInfo = await PackageInfo.fromPlatform(); config = await preferences.getConfig() ?? @@ -244,14 +258,17 @@ class GlobalState { getUpdateConfigParams([bool? isPatch]) { final currentProfile = config.currentProfile; final clashConfig = config.patchClashConfig; + final routeAddress = + config.networkProps.routeMode == RouteMode.bypassPrivate + ? defaultBypassPrivateRouteAddress + : clashConfig.tun.routeAddress; return UpdateConfigParams( profileId: config.currentProfileId ?? "", config: clashConfig.copyWith( globalUa: ua, tun: clashConfig.tun.copyWith( - routeAddress: config.networkProps.routeMode == RouteMode.bypassPrivate - ? defaultBypassPrivateRouteAddress - : clashConfig.tun.routeAddress, + autoRoute: routeAddress.isEmpty ? true : false, + routeAddress: routeAddress, ), rule: currentProfile?.overrideData.runningRule ?? [], ), diff --git a/lib/widgets/color_scheme_box.dart b/lib/widgets/color_scheme_box.dart index f9af319..9f9ea63 100644 --- a/lib/widgets/color_scheme_box.dart +++ b/lib/widgets/color_scheme_box.dart @@ -103,7 +103,7 @@ class PrimaryColorBox extends ConsumerWidget { genColorSchemeProvider( themeData.brightness, color: primaryColor, - isOverride: true, + ignoreConfig: true, ), ); return Theme( diff --git a/lib/widgets/container.dart b/lib/widgets/container.dart new file mode 100644 index 0000000..b01d587 --- /dev/null +++ b/lib/widgets/container.dart @@ -0,0 +1,73 @@ +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class CommonSafeArea extends StatelessWidget { + const CommonSafeArea({ + super.key, + this.left = true, + this.top = true, + this.right = true, + this.bottom = true, + this.minimum = EdgeInsets.zero, + this.maintainBottomViewPadding = false, + required this.child, + }); + + final bool left; + + final bool top; + + final bool right; + + final bool bottom; + + final EdgeInsets minimum; + + final bool maintainBottomViewPadding; + + final Widget child; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + EdgeInsets padding = MediaQuery.paddingOf(context); + final height = MediaQuery.of(context).size.height; + if (maintainBottomViewPadding) { + padding = padding.copyWith( + bottom: MediaQuery.viewPaddingOf(context).bottom, + ); + } + final double realPaddingTop = padding.top > height * 0.5 ? 0 : padding.top; + return Padding( + padding: EdgeInsets.only( + left: math.max(left ? padding.left : 0.0, minimum.left), + top: math.max(top ? realPaddingTop : 0.0, minimum.top), + right: math.max(right ? padding.right : 0.0, minimum.right), + bottom: math.max(bottom ? padding.bottom : 0.0, minimum.bottom), + ), + child: MediaQuery.removePadding( + context: context, + removeLeft: left, + removeTop: top, + removeRight: right, + removeBottom: bottom, + child: child, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(FlagProperty('left', value: left, ifTrue: 'avoid left padding')); + properties + .add(FlagProperty('top', value: top, ifTrue: 'avoid top padding')); + properties.add( + FlagProperty('right', value: right, ifTrue: 'avoid right padding')); + properties.add( + FlagProperty('bottom', value: bottom, ifTrue: 'avoid bottom padding')); + } +} diff --git a/lib/widgets/donut_chart.dart b/lib/widgets/donut_chart.dart index 3e41685..e78e686 100644 --- a/lib/widgets/donut_chart.dart +++ b/lib/widgets/donut_chart.dart @@ -137,7 +137,7 @@ class DonutChartPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); - const strokeWidth = 10.0; + final strokeWidth = 10.0.ap; final radius = min(size.width / 2, size.height / 2) - strokeWidth / 2; final gapAngle = 2 * asin(strokeWidth * 1 / (2 * radius)) * 1.2; diff --git a/lib/widgets/fade_box.dart b/lib/widgets/fade_box.dart index 956e17c..8a69be6 100644 --- a/lib/widgets/fade_box.dart +++ b/lib/widgets/fade_box.dart @@ -36,11 +36,13 @@ class FadeBox extends StatelessWidget { class FadeThroughBox extends StatelessWidget { final Widget child; final Alignment? alignment; + final EdgeInsets? margin; const FadeThroughBox({ super.key, required this.child, this.alignment, + this.margin }); @override @@ -52,6 +54,7 @@ class FadeThroughBox extends StatelessWidget { secondaryAnimation, ) { return Container( + margin: margin, alignment: alignment ?? Alignment.centerLeft, child: FadeThroughTransition( animation: animation, diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index f02b558..377b0ed 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -62,6 +62,22 @@ class OpenDelegate extends Delegate { }); } +class NextDelegate extends Delegate { + final Widget widget; + final String title; + final double? maxWidth; + final Widget? action; + final bool blur; + + const NextDelegate({ + required this.title, + required this.widget, + this.maxWidth, + this.action, + this.blur = true, + }); +} + class OptionsDelegate extends Delegate { final List options; final String title; @@ -138,6 +154,21 @@ class ListItem extends StatelessWidget { this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTap = null; + const ListItem.next({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.trailing, + required NextDelegate this.delegate, + this.horizontalTitleGap, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, + this.tileTitleAlignment = ListTileTitleAlignment.center, + }) : onTap = null; + const ListItem.options({ super.key, required this.title, @@ -226,6 +257,7 @@ class ListItem extends StatelessWidget { leading: leading ?? this.leading, horizontalTitleGap: horizontalTitleGap, title: title, + minVerticalPadding: 12, subtitle: subtitle, titleAlignment: tileTitleAlignment, onTap: onTap, @@ -285,6 +317,34 @@ class ListItem extends StatelessWidget { }, ); } + if (delegate is NextDelegate) { + final nextDelegate = delegate as NextDelegate; + final child = SafeArea( + child: nextDelegate.widget, + ); + + return _buildListTile( + onTap: () { + showExtend( + context, + props: ExtendProps( + blur: nextDelegate.blur, + maxWidth: nextDelegate.maxWidth, + ), + builder: (_, type) { + return AdaptiveSheetScaffold( + actions: [ + if (nextDelegate.action != null) nextDelegate.action!, + ], + type: type, + body: child, + title: nextDelegate.title, + ); + }, + ); + }, + ); + } if (delegate is OptionsDelegate) { final optionsDelegate = delegate as OptionsDelegate; return _buildListTile( @@ -353,14 +413,11 @@ class ListItem extends StatelessWidget { radioDelegate.onChanged!(radioDelegate.value); } }, - leading: SizedBox( - width: 32, - height: 32, - child: Radio( - value: radioDelegate.value, - groupValue: radioDelegate.groupValue, - onChanged: radioDelegate.onChanged, - ), + leading: Radio( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: radioDelegate.value, + groupValue: radioDelegate.groupValue, + onChanged: radioDelegate.onChanged, ), trailing: trailing, ); @@ -466,6 +523,32 @@ List generateSection({ ]; } +Widget generateSectionV2({ + String? title, + required Iterable items, + List? actions, + bool separated = true, +}) { + return Column( + children: [ + if (items.isNotEmpty && title != null) + ListHeader( + title: title, + actions: actions, + ), + CommonCard( + radius: 18, + type: CommonCardType.filled, + child: Column( + children: [ + ...items, + ], + ), + ) + ], + ); +} + List generateInfoSection({ required Info info, required Iterable items, @@ -497,4 +580,4 @@ Widget generateListView(List items) { bottom: 16, ), ); -} \ No newline at end of file +} diff --git a/lib/widgets/notification.dart b/lib/widgets/notification.dart new file mode 100644 index 0000000..5d87140 --- /dev/null +++ b/lib/widgets/notification.dart @@ -0,0 +1,33 @@ +import 'package:fl_clash/models/config.dart'; +import 'package:fl_clash/providers/config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TextScaleNotification extends StatelessWidget { + final Widget child; + final Function(TextScale textScale) onNotification; + + const TextScaleNotification({ + super.key, + required this.child, + required this.onNotification, + }); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, ref, child) { + ref.listen( + themeSettingProvider.select((state) => state.textScale), + (prev, next) { + if (prev != next) { + onNotification(next); + } + }, + ); + return child!; + }, + child: child, + ); + } +} diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 1451cc8..df858a9 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -10,7 +10,7 @@ import 'package:flutter/services.dart'; import 'chip.dart'; class CommonScaffold extends StatefulWidget { - final PreferredSizeWidget? appBar; + final AppBar? appBar; final Widget body; final Widget? bottomNavigationBar; final Widget? sideNavigationBar; @@ -125,25 +125,25 @@ class CommonScaffoldState extends State { } } - ThemeData _appBarTheme(BuildContext context) { + Widget _buildSearchingAppBarTheme(Widget child) { final ThemeData theme = Theme.of(context); final ColorScheme colorScheme = theme.colorScheme; - return theme.copyWith( - appBarTheme: AppBarTheme( - systemOverlayStyle: colorScheme.brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - backgroundColor: colorScheme.brightness == Brightness.dark - ? Colors.grey[900] - : Colors.white, - iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), - titleTextStyle: theme.textTheme.titleLarge, - toolbarTextStyle: theme.textTheme.bodyMedium, - ), - inputDecorationTheme: InputDecorationTheme( - hintStyle: theme.inputDecorationTheme.hintStyle, - border: InputBorder.none, + return Theme( + data: theme.copyWith( + appBarTheme: theme.appBarTheme.copyWith( + backgroundColor: colorScheme.brightness == Brightness.dark + ? Colors.grey[900] + : Colors.white, + iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), + titleTextStyle: theme.textTheme.titleLarge, + toolbarTextStyle: theme.textTheme.bodyMedium, + ), + inputDecorationTheme: InputDecorationTheme( + hintStyle: theme.inputDecorationTheme.hintStyle, + border: InputBorder.none, + ), ), + child: child, ); } @@ -318,72 +318,66 @@ class CommonScaffoldState extends State { child: appBar, ); } - return _isSearch - ? Theme( - data: _appBarTheme(context), - child: CommonPopScope( - onPop: () { - if (_isSearch) { - _handleExitSearching(); - return false; - } - return true; - }, - child: appBar, - ), - ) - : appBar; + return _isSearch ? _buildSearchingAppBarTheme(appBar) : appBar; } PreferredSizeWidget _buildAppBar() { return PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - ValueListenableBuilder( - valueListenable: _appBarState, - builder: (_, state, __) { - return _buildAppBarWrap( - AppBar( - centerTitle: widget.centerTitle ?? false, - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - systemNavigationBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - systemNavigationBarColor: widget.bottomNavigationBar != null - ? context.colorScheme.surfaceContainer - : context.colorScheme.surface, - systemNavigationBarDividerColor: Colors.transparent, - ), - automaticallyImplyLeading: widget.automaticallyImplyLeading, - leading: _buildLeading(), - title: _buildTitle(state.searchState), - actions: _buildActions( - state.searchState != null, - state.actions.isNotEmpty - ? state.actions - : widget.actions ?? [], - ), + child: Theme( + data: Theme.of(context).copyWith( + appBarTheme: AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: + Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + systemNavigationBarIconBrightness: + Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + systemNavigationBarColor: widget.bottomNavigationBar != null + ? context.colorScheme.surfaceContainer + : context.colorScheme.surface, + systemNavigationBarDividerColor: Colors.transparent, + ), + ), + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + widget.appBar ?? + ValueListenableBuilder( + valueListenable: _appBarState, + builder: (_, state, __) { + return _buildAppBarWrap( + AppBar( + centerTitle: widget.centerTitle ?? false, + automaticallyImplyLeading: + widget.automaticallyImplyLeading, + leading: _buildLeading(), + title: _buildTitle(state.searchState), + actions: _buildActions( + state.searchState != null, + state.actions.isNotEmpty + ? state.actions + : widget.actions ?? [], + ), + ), + ); + }, ), - ); - }, - ), - ValueListenableBuilder( - valueListenable: _loading, - builder: (_, value, __) { - return value == true - ? const LinearProgressIndicator() - : Container(); - }, - ), - ], + ValueListenableBuilder( + valueListenable: _loading, + builder: (_, value, __) { + return value == true + ? const LinearProgressIndicator() + : Container(); + }, + ), + ], + ), ), ); } @@ -391,56 +385,62 @@ class CommonScaffoldState extends State { @override Widget build(BuildContext context) { assert(widget.appBar != null || widget.title != null); - final body = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ValueListenableBuilder( - valueListenable: _keywordsNotifier, - builder: (_, keywords, __) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_onKeywordsUpdate != null) { - _onKeywordsUpdate!(keywords); + final body = SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: _keywordsNotifier, + builder: (_, keywords, __) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_onKeywordsUpdate != null) { + _onKeywordsUpdate!(keywords); + } + }); + if (keywords.isEmpty) { + return SizedBox(); } - }); - if (keywords.isEmpty) { - return SizedBox(); - } - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Wrap( - runSpacing: 8, - spacing: 8, - children: [ - for (final keyword in keywords) - CommonChip( - label: keyword, - type: ChipType.delete, - onPressed: () { - _deleteKeyword(keyword); - }, - ), - ], - ), - ); - }, - ), - Expanded( - child: widget.body, - ), - ], + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Wrap( + runSpacing: 8, + spacing: 8, + children: [ + for (final keyword in keywords) + CommonChip( + label: keyword, + type: ChipType.delete, + onPressed: () { + _deleteKeyword(keyword); + }, + ), + ], + ), + ); + }, + ), + Expanded( + child: widget.body, + ), + ], + ), ); final scaffold = Scaffold( - appBar: widget.appBar ?? _buildAppBar(), + appBar: _buildAppBar(), body: body, backgroundColor: widget.backgroundColor, floatingActionButton: ValueListenableBuilder( valueListenable: _floatingActionButton, builder: (_, value, __) { - return FadeScaleBox( - child: value ?? SizedBox(), + return IntrinsicWidth( + child: IntrinsicHeight( + child: FadeScaleBox( + child: value ?? SizedBox(), + ), + ), ); }, ), diff --git a/lib/widgets/scroll.dart b/lib/widgets/scroll.dart index 461afd6..5e06454 100644 --- a/lib/widgets/scroll.dart +++ b/lib/widgets/scroll.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/common/list.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -54,13 +54,13 @@ class ScrollToEndBox extends StatefulWidget { final ScrollController controller; final List dataSource; final Widget child; - final Key cacheKey; + final CacheTag tag; const ScrollToEndBox({ super.key, required this.child, required this.controller, - required this.cacheKey, + required this.tag, required this.dataSource, }); @@ -73,8 +73,7 @@ class _ScrollToEndBoxState extends State> { _handleTryToEnd() { WidgetsBinding.instance.addPostFrameCallback((_) { - final double offset = - globalState.cacheScrollPosition[widget.cacheKey] ?? -1; + final double offset = globalState.cacheScrollPosition[widget.tag] ?? -1; if (offset < 0) { widget.controller.animateTo( duration: kThemeAnimationDuration, @@ -85,12 +84,6 @@ class _ScrollToEndBoxState extends State> { }); } - @override - void initState() { - super.initState(); - globalState.cacheScrollPosition[widget.cacheKey] = -1; - } - @override void didUpdateWidget(ScrollToEndBox oldWidget) { super.didUpdateWidget(oldWidget); @@ -101,13 +94,12 @@ class _ScrollToEndBoxState extends State> { @override Widget build(BuildContext context) { - return NotificationListener( + return NotificationListener( onNotification: (details) { - double offset = + globalState.cacheScrollPosition[widget.tag] = details.metrics.pixels == details.metrics.maxScrollExtent ? -1 : details.metrics.pixels; - globalState.cacheScrollPosition[widget.cacheKey] = offset; return false; }, child: widget.child, @@ -124,6 +116,7 @@ class CacheItemExtentListView extends StatefulWidget { final bool shrinkWrap; final bool reverse; final ScrollController controller; + final CacheTag tag; const CacheItemExtentListView({ super.key, @@ -135,6 +128,7 @@ class CacheItemExtentListView extends StatefulWidget { required this.keyBuilder, required this.itemCount, required this.itemExtentBuilder, + required this.tag, }); @override @@ -143,21 +137,19 @@ class CacheItemExtentListView extends StatefulWidget { } class CacheItemExtentListViewState extends State { - late final FixedMap _cacheHeightMap; - @override void initState() { super.initState(); - _cacheHeightMap = FixedMap(widget.itemCount); + _updateCacheHeightMap(); } - clearCache() { - _cacheHeightMap.clear(); + _updateCacheHeightMap() { + globalState.cacheHeightMap[widget.tag]?.updateMaxLength(widget.itemCount); + globalState.cacheHeightMap[widget.tag] ??= FixedMap(widget.itemCount); } @override Widget build(BuildContext context) { - _cacheHeightMap.updateMaxSize(widget.itemCount); return ListView.builder( itemBuilder: widget.itemBuilder, itemCount: widget.itemCount, @@ -166,20 +158,14 @@ class CacheItemExtentListViewState extends State { shrinkWrap: widget.shrinkWrap, controller: widget.controller, itemExtentBuilder: (index, __) { - final key = widget.keyBuilder(index); - if (_cacheHeightMap.containsKey(key)) { - return _cacheHeightMap.get(key); - } - return _cacheHeightMap.put(key, widget.itemExtentBuilder(index)); + _updateCacheHeightMap(); + return globalState.cacheHeightMap[widget.tag]?.updateCacheValue( + widget.keyBuilder(index), + () => widget.itemExtentBuilder(index), + ); }, ); } - - @override - void dispose() { - _cacheHeightMap.clear(); - super.dispose(); - } } class CacheItemExtentSliverReorderableList extends StatefulWidget { @@ -189,6 +175,7 @@ class CacheItemExtentSliverReorderableList extends StatefulWidget { final double Function(int index) itemExtentBuilder; final ReorderCallback onReorder; final ReorderItemProxyDecorator? proxyDecorator; + final CacheTag tag; const CacheItemExtentSliverReorderableList({ super.key, @@ -198,6 +185,7 @@ class CacheItemExtentSliverReorderableList extends StatefulWidget { required this.itemExtentBuilder, required this.onReorder, this.proxyDecorator, + required this.tag, }); @override @@ -207,30 +195,24 @@ class CacheItemExtentSliverReorderableList extends StatefulWidget { class CacheItemExtentSliverReorderableListState extends State { - late final FixedMap _cacheHeightMap; - @override void initState() { super.initState(); - _cacheHeightMap = FixedMap(widget.itemCount); - } - - clearCache() { - _cacheHeightMap.clear(); + globalState.cacheHeightMap[widget.tag]?.updateMaxLength(widget.itemCount); + globalState.cacheHeightMap[widget.tag] ??= FixedMap(widget.itemCount); } @override Widget build(BuildContext context) { - _cacheHeightMap.updateMaxSize(widget.itemCount); + globalState.cacheHeightMap[widget.tag]?.updateMaxLength(widget.itemCount); return SliverReorderableList( itemBuilder: widget.itemBuilder, itemCount: widget.itemCount, itemExtentBuilder: (index, __) { - final key = widget.keyBuilder(index); - if (_cacheHeightMap.containsKey(key)) { - return _cacheHeightMap.get(key); - } - return _cacheHeightMap.put(key, widget.itemExtentBuilder(index)); + return globalState.cacheHeightMap[widget.tag]?.updateCacheValue( + widget.keyBuilder(index), + () => widget.itemExtentBuilder(index), + ); }, onReorder: widget.onReorder, proxyDecorator: widget.proxyDecorator, @@ -239,7 +221,6 @@ class CacheItemExtentSliverReorderableListState @override void dispose() { - _cacheHeightMap.clear(); super.dispose(); } } diff --git a/lib/widgets/super_grid.dart b/lib/widgets/super_grid.dart index a8336fd..8d03394 100644 --- a/lib/widgets/super_grid.dart +++ b/lib/widgets/super_grid.dart @@ -369,6 +369,7 @@ class SuperGridState extends State with TickerProviderStateMixin { } _handleDelete(int index) async { + await _transformCompleter?.future; _preTransformState(); final indexWhere = _tempIndexList.indexWhere((i) => i == index); _tempIndexList.removeAt(indexWhere); @@ -484,9 +485,24 @@ class SuperGridState extends State with TickerProviderStateMixin { Widget _draggableWrap({ required Widget childWhenDragging, required Widget feedback, - required Widget target, + required Widget item, required int index, }) { + final target = DragTarget( + builder: (_, __, ___) { + return AbsorbPointer( + child: item, + ); + }, + onWillAcceptWithDetails: (_) { + debouncer.call( + DebounceTag.handleWill, + _handleWill, + args: [index], + ); + return false; + }, + ); final shakeTarget = ValueListenableBuilder( valueListenable: _dragIndexNotifier, builder: (_, dragIndex, child) { @@ -539,7 +555,7 @@ class SuperGridState extends State with TickerProviderStateMixin { valueListenable: isEditNotifier, builder: (_, isEdit, child) { if (!isEdit) { - return target; + return item; } return child!; }, @@ -558,12 +574,10 @@ class SuperGridState extends State with TickerProviderStateMixin { _itemContexts[index] = context; final childWhenDragging = ActivateBox( child: Opacity( - opacity: 0.3, + opacity: 0.6, child: _sizeBoxWrap( CommonCard( - child: Container( - color: context.colorScheme.secondaryContainer, - ), + child: child, ), index, ), @@ -580,25 +594,11 @@ class SuperGridState extends State with TickerProviderStateMixin { index, ), ); - final target = DragTarget( - builder: (_, __, ___) { - return child; - }, - onWillAcceptWithDetails: (_) { - debouncer.call( - DebounceTag.handleWill, - _handleWill, - args: [index], - ); - return false; - }, - ); - return _wrapTransform( _draggableWrap( childWhenDragging: childWhenDragging, feedback: feedback, - target: target, + item: child, index: index, ), index, @@ -666,8 +666,7 @@ class SuperGridState extends State with TickerProviderStateMixin { crossAxisSpacing: widget.crossAxisSpacing, mainAxisSpacing: widget.mainAxisSpacing, children: [ - for (int i = 0; i < children.length; i++) - _builderItem(i), + for (int i = 0; i < children.length; i++) _builderItem(i), ], ); }, diff --git a/lib/widgets/tab.dart b/lib/widgets/tab.dart new file mode 100644 index 0000000..9b4fe79 --- /dev/null +++ b/lib/widgets/tab.dart @@ -0,0 +1,1100 @@ +import 'dart:math' as math; +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/rendering.dart'; + +const EdgeInsetsGeometry _kHorizontalItemPadding = + EdgeInsets.symmetric(vertical: 2, horizontal: 3); + +const Radius _kCornerRadius = Radius.circular(9); + +const Radius _kThumbRadius = Radius.circular(8); + +const EdgeInsets _kThumbInsets = EdgeInsets.symmetric(horizontal: 1); + +const double _kMinSegmentedControlHeight = 28.0; + +const EdgeInsets _kSeparatorInset = EdgeInsets.symmetric(vertical: 5); + +const double _kSeparatorWidth = 1; + +const double _kMinThumbScale = 0.95; + +const double _kSegmentMinPadding = 10; + +const double _kTouchYDistanceThreshold = 50.0 * 50.0; + +const double _kContentPressedMinOpacity = 0.2; + +const double _kFontSize = 13.0; + +const FontWeight _kFontWeight = FontWeight.w500; + +const FontWeight _kHighlightedFontWeight = FontWeight.w600; + +const Color _kDisabledContentColor = Color.fromARGB(115, 122, 122, 122); + +final SpringSimulation _kThumbSpringAnimationSimulation = SpringSimulation( + const SpringDescription(mass: 1, stiffness: 503.551, damping: 44.8799), + 0, + 1, + 0, +); + +const Duration _kSpringAnimationDuration = Duration(milliseconds: 412); + +const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470); + +const Duration _kHighlightAnimationDuration = Duration(milliseconds: 200); + +class CommonTabBar extends StatefulWidget { + CommonTabBar({ + super.key, + required this.children, + required this.onValueChanged, + this.disabledChildren = const {}, + this.groupValue, + required this.thumbColor, + this.padding = _kHorizontalItemPadding, + this.backgroundColor, + this.proportionalWidth = false, + }) : assert(children.length >= 2), + assert( + groupValue == null || children.keys.contains(groupValue), + 'The groupValue must be either null or one of the keys in the children map.', + ); + final Map children; + final Set disabledChildren; + final T? groupValue; + final ValueChanged onValueChanged; + final Color? backgroundColor; + final Color thumbColor; + final bool proportionalWidth; + final EdgeInsetsGeometry padding; + + @override + State> createState() => _CommonTabBarState(); +} + +class _CommonTabBarState extends State> + with TickerProviderStateMixin> { + late final AnimationController thumbController = AnimationController( + duration: _kSpringAnimationDuration, + value: 0, + vsync: this, + ); + Animatable? thumbAnimatable; + + late final AnimationController thumbScaleController = AnimationController( + duration: _kSpringAnimationDuration, + value: 0, + vsync: this, + ); + late Animation thumbScaleAnimation = thumbScaleController.drive( + Tween(begin: 1, end: _kMinThumbScale), + ); + + final TapGestureRecognizer tap = TapGestureRecognizer(); + final HorizontalDragGestureRecognizer drag = + HorizontalDragGestureRecognizer(); + final LongPressGestureRecognizer longPress = LongPressGestureRecognizer(); + final GlobalKey segmentedControlRenderWidgetKey = GlobalKey(); + + @override + void initState() { + super.initState(); + final GestureArenaTeam team = GestureArenaTeam(); + longPress.team = team; + drag.team = team; + team.captain = drag; + + drag + ..onDown = onDown + ..onUpdate = onUpdate + ..onEnd = onEnd + ..onCancel = onCancel; + + tap.onTapUp = onTapUp; + longPress.onLongPress = () {}; + + highlighted = widget.groupValue; + } + + @override + void didUpdateWidget(CommonTabBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (!isThumbDragging && highlighted != widget.groupValue) { + thumbController.animateWith(_kThumbSpringAnimationSimulation); + thumbAnimatable = null; + highlighted = widget.groupValue; + } + } + + @override + void dispose() { + thumbScaleController.dispose(); + thumbController.dispose(); + + drag.dispose(); + tap.dispose(); + longPress.dispose(); + + super.dispose(); + } + + bool? _startedOnSelectedSegment; + bool _startedOnDisabledSegment = false; + + bool get isThumbDragging => + (_startedOnSelectedSegment ?? false) && !_startedOnDisabledSegment; + + T segmentForXPosition(double dx) { + final BuildContext currentContext = + segmentedControlRenderWidgetKey.currentContext!; + final _RenderSegmentedControl renderBox = + currentContext.findRenderObject()! as _RenderSegmentedControl; + + final int numOfChildren = widget.children.length; + assert(renderBox.hasSize); + assert(numOfChildren >= 2); + + int segmentIndex = renderBox.getClosestSegmentIndex(dx); + + switch (Directionality.of(context)) { + case TextDirection.ltr: + break; + case TextDirection.rtl: + segmentIndex = numOfChildren - 1 - segmentIndex; + } + return widget.children.keys.elementAt(segmentIndex); + } + + bool _hasDraggedTooFar(DragUpdateDetails details) { + final RenderBox renderBox = context.findRenderObject()! as RenderBox; + assert(renderBox.hasSize); + final Size size = renderBox.size; + final Offset offCenter = + details.localPosition - Offset(size.width / 2, size.height / 2); + final double l2 = + math.pow(math.max(0.0, offCenter.dx.abs() - size.width / 2), 2) + + math.pow(math.max(0.0, offCenter.dy.abs() - size.height / 2), 2) + as double; + return l2 > _kTouchYDistanceThreshold; + } + + void _playThumbScaleAnimation({required bool isExpanding}) { + thumbScaleAnimation = thumbScaleController.drive( + Tween( + begin: thumbScaleAnimation.value, + end: isExpanding ? 1 : _kMinThumbScale), + ); + thumbScaleController.animateWith(_kThumbSpringAnimationSimulation); + } + + void onHighlightChangedByGesture(T newValue) { + if (highlighted == newValue) { + return; + } + + setState(() { + highlighted = newValue; + }); + thumbController.animateWith(_kThumbSpringAnimationSimulation); + thumbAnimatable = null; + } + + void onPressedChangedByGesture(T? newValue) { + if (pressed != newValue) { + setState(() { + pressed = newValue; + }); + } + } + + void onTapUp(TapUpDetails details) { + if (isThumbDragging) { + return; + } + final T segment = segmentForXPosition(details.localPosition.dx); + onPressedChangedByGesture(null); + if (segment != widget.groupValue && + !widget.disabledChildren.contains(segment)) { + widget.onValueChanged(segment); + } + } + + void onDown(DragDownDetails details) { + final T touchDownSegment = segmentForXPosition(details.localPosition.dx); + _startedOnSelectedSegment = touchDownSegment == highlighted; + _startedOnDisabledSegment = + widget.disabledChildren.contains(touchDownSegment); + if (widget.disabledChildren.contains(touchDownSegment)) { + return; + } + onPressedChangedByGesture(touchDownSegment); + + if (isThumbDragging) { + _playThumbScaleAnimation(isExpanding: false); + } + } + + void onUpdate(DragUpdateDetails details) { + if (_startedOnDisabledSegment) { + return; + } + final T touchDownSegment = segmentForXPosition(details.localPosition.dx); + if (widget.disabledChildren.contains(touchDownSegment)) { + return; + } + if (isThumbDragging) { + onPressedChangedByGesture(touchDownSegment); + onHighlightChangedByGesture(touchDownSegment); + } else { + final T? segment = _hasDraggedTooFar(details) + ? null + : segmentForXPosition(details.localPosition.dx); + onPressedChangedByGesture(segment); + } + } + + void onEnd(DragEndDetails details) { + final T? pressed = this.pressed; + if (isThumbDragging) { + _playThumbScaleAnimation(isExpanding: true); + if (highlighted != widget.groupValue) { + widget.onValueChanged(highlighted); + } + } else if (pressed != null) { + onHighlightChangedByGesture(pressed); + assert(pressed == highlighted); + if (highlighted != widget.groupValue) { + widget.onValueChanged(highlighted); + } + } + + onPressedChangedByGesture(null); + _startedOnSelectedSegment = null; + } + + void onCancel() { + if (isThumbDragging) { + _playThumbScaleAnimation(isExpanding: true); + } + onPressedChangedByGesture(null); + _startedOnSelectedSegment = null; + } + + T? highlighted; + + T? pressed; + + @override + Widget build(BuildContext context) { + assert(widget.children.length >= 2); + List children = []; + bool isPreviousSegmentHighlighted = false; + + int index = 0; + int? highlightedIndex; + for (final MapEntry entry in widget.children.entries) { + final bool isHighlighted = highlighted == entry.key; + if (isHighlighted) { + highlightedIndex = index; + } + + if (index != 0) { + children.add( + _SegmentSeparator( + key: ValueKey(index), + highlighted: isPreviousSegmentHighlighted || isHighlighted, + ), + ); + } + + final TextDirection textDirection = Directionality.of(context); + final _SegmentLocation segmentLocation = switch (textDirection) { + TextDirection.ltr when index == 0 => _SegmentLocation.leftmost, + TextDirection.ltr when index == widget.children.length - 1 => + _SegmentLocation.rightmost, + TextDirection.rtl when index == widget.children.length - 1 => + _SegmentLocation.leftmost, + TextDirection.rtl when index == 0 => _SegmentLocation.rightmost, + TextDirection.ltr || TextDirection.rtl => _SegmentLocation.inbetween, + }; + children.add( + Semantics( + button: true, + onTap: () { + if (widget.disabledChildren.contains(entry.key)) { + return; + } + widget.onValueChanged(entry.key); + }, + inMutuallyExclusiveGroup: true, + selected: widget.groupValue == entry.key, + child: MouseRegion( + cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, + child: _Segment( + key: ValueKey(entry.key), + highlighted: isHighlighted, + pressed: pressed == entry.key, + isDragging: isThumbDragging, + enabled: !widget.disabledChildren.contains(entry.key), + segmentLocation: segmentLocation, + child: entry.value, + ), + ), + ), + ); + + index += 1; + isPreviousSegmentHighlighted = isHighlighted; + } + + assert((highlightedIndex == null) == (highlighted == null)); + + switch (Directionality.of(context)) { + case TextDirection.ltr: + break; + case TextDirection.rtl: + children = children.reversed.toList(growable: false); + if (highlightedIndex != null) { + highlightedIndex = index - 1 - highlightedIndex; + } + } + + return UnconstrainedBox( + constrainedAxis: Axis.horizontal, + child: Container( + clipBehavior: Clip.antiAlias, + padding: widget.padding.resolve(Directionality.of(context)), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(_kCornerRadius), + color: widget.backgroundColor, + ), + child: AnimatedBuilder( + animation: thumbScaleAnimation, + builder: (BuildContext context, Widget? child) { + return _CommonTabBarRenderWidget( + proportionalWidth: widget.proportionalWidth, + key: segmentedControlRenderWidgetKey, + highlightedIndex: highlightedIndex, + thumbColor: widget.thumbColor, + thumbScale: thumbScaleAnimation.value, + state: this, + children: children, + ); + }, + ), + ), + ); + } +} + +class _Segment extends StatefulWidget { + const _Segment({ + required ValueKey key, + required this.child, + required this.pressed, + required this.highlighted, + required this.isDragging, + required this.enabled, + required this.segmentLocation, + }) : super(key: key); + + final Widget child; + + final bool pressed; + final bool highlighted; + final bool enabled; + final _SegmentLocation segmentLocation; + final bool isDragging; + + bool get shouldFadeoutContent => pressed && !highlighted && enabled; + + bool get shouldScaleContent => + pressed && highlighted && isDragging && enabled; + + @override + _SegmentState createState() => _SegmentState(); +} + +class _SegmentState extends State<_Segment> + with TickerProviderStateMixin<_Segment> { + late final AnimationController highlightPressScaleController; + late Animation highlightPressScaleAnimation; + + @override + void initState() { + super.initState(); + highlightPressScaleController = AnimationController( + duration: _kOpacityAnimationDuration, + value: widget.shouldScaleContent ? 1 : 0, + vsync: this, + ); + + highlightPressScaleAnimation = highlightPressScaleController.drive( + Tween(begin: 1.0, end: _kMinThumbScale), + ); + } + + @override + void didUpdateWidget(_Segment oldWidget) { + super.didUpdateWidget(oldWidget); + assert(oldWidget.key == widget.key); + + if (oldWidget.shouldScaleContent != widget.shouldScaleContent) { + highlightPressScaleAnimation = highlightPressScaleController.drive( + Tween( + begin: highlightPressScaleAnimation.value, + end: widget.shouldScaleContent ? _kMinThumbScale : 1.0, + ), + ); + highlightPressScaleController + .animateWith(_kThumbSpringAnimationSimulation); + } + } + + @override + void dispose() { + highlightPressScaleController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Alignment scaleAlignment = switch (widget.segmentLocation) { + _SegmentLocation.leftmost => Alignment.centerLeft, + _SegmentLocation.rightmost => Alignment.centerRight, + _SegmentLocation.inbetween => Alignment.center, + }; + + return MetaData( + behavior: HitTestBehavior.opaque, + child: IndexedStack( + alignment: Alignment.center, + children: [ + AnimatedOpacity( + opacity: + widget.shouldFadeoutContent ? _kContentPressedMinOpacity : 1, + duration: _kOpacityAnimationDuration, + curve: Curves.ease, + child: AnimatedDefaultTextStyle( + style: DefaultTextStyle.of(context).style.merge( + TextStyle( + fontWeight: widget.highlighted + ? _kHighlightedFontWeight + : _kFontWeight, + fontSize: _kFontSize, + color: widget.enabled ? null : _kDisabledContentColor, + ), + ), + duration: _kHighlightAnimationDuration, + curve: Curves.ease, + child: ScaleTransition( + alignment: scaleAlignment, + scale: highlightPressScaleAnimation, + child: widget.child, + ), + ), + ), + DefaultTextStyle.merge( + style: const TextStyle( + fontWeight: _kHighlightedFontWeight, fontSize: _kFontSize), + child: widget.child, + ), + ], + ), + ); + } +} + +class _SegmentSeparator extends StatefulWidget { + const _SegmentSeparator({ + required ValueKey key, + required this.highlighted, + }) : super(key: key); + + final bool highlighted; + + @override + _SegmentSeparatorState createState() => _SegmentSeparatorState(); +} + +class _SegmentSeparatorState extends State<_SegmentSeparator> + with TickerProviderStateMixin<_SegmentSeparator> { + late final AnimationController separatorOpacityController; + + @override + void initState() { + super.initState(); + + separatorOpacityController = AnimationController( + duration: _kSpringAnimationDuration, + value: widget.highlighted ? 0 : 1, + vsync: this, + ); + } + + @override + void didUpdateWidget(_SegmentSeparator oldWidget) { + super.didUpdateWidget(oldWidget); + assert(oldWidget.key == widget.key); + + if (oldWidget.highlighted != widget.highlighted) { + separatorOpacityController.animateTo( + widget.highlighted ? 0 : 1, + duration: _kSpringAnimationDuration, + curve: Curves.ease, + ); + } + } + + @override + void dispose() { + separatorOpacityController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: separatorOpacityController, + child: const SizedBox(width: _kSeparatorWidth), + builder: (BuildContext context, Widget? child) { + return Padding( + padding: _kSeparatorInset, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + ), + child: child, + ), + ); + }, + ); + } +} + +class _CommonTabBarRenderWidget + extends MultiChildRenderObjectWidget { + const _CommonTabBarRenderWidget({ + super.key, + super.children, + required this.highlightedIndex, + required this.thumbColor, + required this.thumbScale, + required this.state, + required this.proportionalWidth, + }); + + final int? highlightedIndex; + final Color thumbColor; + final double thumbScale; + final bool proportionalWidth; + final _CommonTabBarState state; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderSegmentedControl( + highlightedIndex: highlightedIndex, + thumbColor: thumbColor, + thumbScale: thumbScale, + proportionalWidth: proportionalWidth, + state: state, + ); + } + + @override + void updateRenderObject( + BuildContext context, _RenderSegmentedControl renderObject) { + assert(renderObject.state == state); + renderObject + ..thumbColor = thumbColor + ..thumbScale = thumbScale + ..highlightedIndex = highlightedIndex + ..proportionalWidth = proportionalWidth; + } +} + +class _SegmentedControlContainerBoxParentData + extends ContainerBoxParentData {} + +enum _SegmentLocation { leftmost, rightmost, inbetween } + +class _RenderSegmentedControl extends RenderBox + with + ContainerRenderObjectMixin>, + RenderBoxContainerDefaultsMixin> { + _RenderSegmentedControl({ + required int? highlightedIndex, + required Color thumbColor, + required double thumbScale, + required bool proportionalWidth, + required this.state, + }) : _highlightedIndex = highlightedIndex, + _thumbColor = thumbColor, + _thumbScale = thumbScale, + _proportionalWidth = proportionalWidth; + + final _CommonTabBarState state; + + Rect? currentThumbRect; + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + state.thumbController.addListener(markNeedsPaint); + } + + @override + void detach() { + state.thumbController.removeListener(markNeedsPaint); + super.detach(); + } + + double get thumbScale => _thumbScale; + double _thumbScale; + + set thumbScale(double value) { + if (_thumbScale == value) { + return; + } + + _thumbScale = value; + if (state.highlighted != null) { + markNeedsPaint(); + } + } + + int? get highlightedIndex => _highlightedIndex; + int? _highlightedIndex; + + set highlightedIndex(int? value) { + if (_highlightedIndex == value) { + return; + } + + _highlightedIndex = value; + markNeedsPaint(); + } + + Color get thumbColor => _thumbColor; + Color _thumbColor; + + set thumbColor(Color value) { + if (_thumbColor == value) { + return; + } + _thumbColor = value; + markNeedsPaint(); + } + + bool get proportionalWidth => _proportionalWidth; + bool _proportionalWidth; + + set proportionalWidth(bool value) { + if (_proportionalWidth == value) { + return; + } + _proportionalWidth = value; + markNeedsLayout(); + } + + @override + void handleEvent(PointerEvent event, BoxHitTestEntry entry) { + assert(debugHandleEvent(event, entry)); + if (event is PointerDownEvent && !state.isThumbDragging) { + state.tap.addPointer(event); + state.longPress.addPointer(event); + state.drag.addPointer(event); + } + } + + double get separatorWidth => _kSeparatorInset.horizontal + _kSeparatorWidth; + + double get totalSeparatorWidth => separatorWidth * (childCount ~/ 2); + + int getClosestSegmentIndex(double dx) { + int index = 0; + RenderBox? child = firstChild; + while (child != null) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + final double clampX = clampDouble( + dx, + childParentData.offset.dx, + child.size.width + childParentData.offset.dx, + ); + + if (dx <= clampX) { + break; + } + + index++; + child = nonSeparatorChildAfter(child); + } + + final int segmentCount = childCount ~/ 2 + 1; + return min(index, segmentCount - 1); + } + + RenderBox? nonSeparatorChildAfter(RenderBox child) { + final RenderBox? nextChild = childAfter(child); + return nextChild == null ? null : childAfter(nextChild); + } + + @override + double computeMinIntrinsicWidth(double height) { + final int childCount = this.childCount ~/ 2 + 1; + RenderBox? child = firstChild; + double maxMinChildWidth = 0; + while (child != null) { + final double childWidth = child.getMinIntrinsicWidth(height); + maxMinChildWidth = math.max(maxMinChildWidth, childWidth); + child = nonSeparatorChildAfter(child); + } + return (maxMinChildWidth + 2 * _kSegmentMinPadding) * childCount + + totalSeparatorWidth; + } + + @override + double computeMaxIntrinsicWidth(double height) { + final int childCount = this.childCount ~/ 2 + 1; + RenderBox? child = firstChild; + double maxMaxChildWidth = 0; + while (child != null) { + final double childWidth = child.getMaxIntrinsicWidth(height); + maxMaxChildWidth = math.max(maxMaxChildWidth, childWidth); + child = nonSeparatorChildAfter(child); + } + return (maxMaxChildWidth + 2 * _kSegmentMinPadding) * childCount + + totalSeparatorWidth; + } + + @override + double computeMinIntrinsicHeight(double width) { + RenderBox? child = firstChild; + double maxMinChildHeight = _kMinSegmentedControlHeight; + while (child != null) { + final double childHeight = child.getMinIntrinsicHeight(width); + maxMinChildHeight = math.max(maxMinChildHeight, childHeight); + child = nonSeparatorChildAfter(child); + } + return maxMinChildHeight; + } + + @override + double computeMaxIntrinsicHeight(double width) { + RenderBox? child = firstChild; + double maxMaxChildHeight = _kMinSegmentedControlHeight; + while (child != null) { + final double childHeight = child.getMaxIntrinsicHeight(width); + maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight); + child = nonSeparatorChildAfter(child); + } + return maxMaxChildHeight; + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _SegmentedControlContainerBoxParentData) { + child.parentData = _SegmentedControlContainerBoxParentData(); + } + } + + double _getMaxChildHeight(BoxConstraints constraints, double childWidth) { + double maxHeight = _kMinSegmentedControlHeight; + RenderBox? child = firstChild; + while (child != null) { + final double boxHeight = child.getMaxIntrinsicHeight(childWidth); + maxHeight = math.max(maxHeight, boxHeight); + child = nonSeparatorChildAfter(child); + } + return maxHeight; + } + + double _getMaxChildWidth(BoxConstraints constraints) { + final int childCount = this.childCount ~/ 2 + 1; + double childWidth = + (constraints.minWidth - totalSeparatorWidth) / childCount; + RenderBox? child = firstChild; + while (child != null) { + childWidth = math.max( + childWidth, + child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding, + ); + child = nonSeparatorChildAfter(child); + } + return math.min( + childWidth, (constraints.maxWidth - totalSeparatorWidth) / childCount); + } + + List _getChildWidths(BoxConstraints constraints) { + if (!proportionalWidth) { + final double maxChildWidth = _getMaxChildWidth(constraints); + final int segmentCount = childCount ~/ 2 + 1; + return List.filled(segmentCount, maxChildWidth); + } + + final List segmentWidths = []; + RenderBox? child = firstChild; + while (child != null) { + final double childWidth = + child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding; + child = nonSeparatorChildAfter(child); + segmentWidths.add(childWidth); + } + + final double totalWidth = segmentWidths.sum; + final double allowedMaxWidth = constraints.maxWidth - totalSeparatorWidth; + final double allowedMinWidth = constraints.minWidth - totalSeparatorWidth; + + final double scale = + clampDouble(totalWidth, allowedMinWidth, allowedMaxWidth) / totalWidth; + if (scale != 1) { + for (int i = 0; i < segmentWidths.length; i++) { + segmentWidths[i] = segmentWidths[i] * scale; + } + } + return segmentWidths; + } + + Size _computeOverallSize(BoxConstraints constraints) { + final double maxChildHeight = + _getMaxChildHeight(constraints, constraints.maxWidth); + return constraints.constrain( + Size(_getChildWidths(constraints).sum + totalSeparatorWidth, + maxChildHeight), + ); + } + + @override + double? computeDryBaseline( + covariant BoxConstraints constraints, TextBaseline baseline) { + final List segmentWidths = _getChildWidths(constraints); + final double childHeight = + _getMaxChildHeight(constraints, constraints.maxWidth); + + int index = 0; + BaselineOffset baselineOffset = BaselineOffset.noBaseline; + RenderBox? child = firstChild; + while (child != null) { + final BoxConstraints childConstraints = BoxConstraints.tight( + Size(segmentWidths[index], childHeight), + ); + baselineOffset = baselineOffset.minOf( + BaselineOffset(child.getDryBaseline(childConstraints, baseline)), + ); + + child = nonSeparatorChildAfter(child); + index++; + } + + return baselineOffset.offset; + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return _computeOverallSize(constraints); + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + final List segmentWidths = _getChildWidths(constraints); + + final double childHeight = _getMaxChildHeight(constraints, double.infinity); + final BoxConstraints separatorConstraints = BoxConstraints( + minHeight: childHeight, + maxHeight: childHeight, + ); + RenderBox? child = firstChild; + int index = 0; + double start = 0; + while (child != null) { + final BoxConstraints childConstraints = BoxConstraints.tight( + Size(segmentWidths[index ~/ 2], childHeight), + ); + child.layout(index.isEven ? childConstraints : separatorConstraints, + parentUsesSize: true); + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + final Offset childOffset = Offset(start, 0); + childParentData.offset = childOffset; + start += child.size.width; + assert( + index.isEven || + child.size.width == _kSeparatorWidth + _kSeparatorInset.horizontal, + '${child.size.width} != ${_kSeparatorWidth + _kSeparatorInset.horizontal}', + ); + child = childAfter(child); + index += 1; + } + size = _computeOverallSize(constraints); + } + + Rect? moveThumbRectInBound(Rect? thumbRect, List children) { + assert(hasSize); + assert(children.length >= 2); + if (thumbRect == null) { + return null; + } + + final Offset firstChildOffset = + (children.first.parentData! as _SegmentedControlContainerBoxParentData) + .offset; + final double leftMost = firstChildOffset.dx; + final double rightMost = + (children.last.parentData! as _SegmentedControlContainerBoxParentData) + .offset + .dx + + children.last.size.width; + assert(rightMost > leftMost); + return Rect.fromLTRB( + math.max(thumbRect.left, leftMost - _kThumbInsets.left), + firstChildOffset.dy - _kThumbInsets.top, + math.min(thumbRect.right, rightMost + _kThumbInsets.right), + firstChildOffset.dy + children.first.size.height + _kThumbInsets.bottom, + ); + } + + @override + void paint(PaintingContext context, Offset offset) { + final List children = getChildrenAsList(); + for (int index = 1; index < childCount; index += 2) { + _paintSeparator(context, offset, children[index]); + } + + final int? highlightedChildIndex = highlightedIndex; + if (highlightedChildIndex != null) { + final RenderBox selectedChild = children[highlightedChildIndex * 2]; + + final _SegmentedControlContainerBoxParentData childParentData = + selectedChild.parentData! as _SegmentedControlContainerBoxParentData; + final Rect newThumbRect = _kThumbInsets.inflateRect( + childParentData.offset & selectedChild.size, + ); + if (state.thumbController.isAnimating) { + final Animatable? thumbTween = state.thumbAnimatable; + if (thumbTween == null) { + final Rect startingRect = + moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect; + state.thumbAnimatable = + RectTween(begin: startingRect, end: newThumbRect); + } else if (newThumbRect != thumbTween.transform(1)) { + final Rect startingRect = + moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect; + state.thumbAnimatable = RectTween( + begin: startingRect, + end: newThumbRect, + ).chain(CurveTween(curve: Interval(state.thumbController.value, 1))); + } + } else { + state.thumbAnimatable = null; + } + + final Rect unscaledThumbRect = + state.thumbAnimatable?.evaluate(state.thumbController) ?? + newThumbRect; + currentThumbRect = unscaledThumbRect; + + final _SegmentLocation childLocation; + if (highlightedChildIndex == 0) { + childLocation = _SegmentLocation.leftmost; + } else if (highlightedChildIndex == children.length ~/ 2) { + childLocation = _SegmentLocation.rightmost; + } else { + childLocation = _SegmentLocation.inbetween; + } + final double delta = switch (childLocation) { + _SegmentLocation.leftmost => + unscaledThumbRect.width - unscaledThumbRect.width * thumbScale, + _SegmentLocation.rightmost => + unscaledThumbRect.width * thumbScale - unscaledThumbRect.width, + _SegmentLocation.inbetween => 0, + }; + final Rect thumbRect = Rect.fromCenter( + center: unscaledThumbRect.center - Offset(delta / 2, 0), + width: unscaledThumbRect.width * thumbScale, + height: unscaledThumbRect.height * thumbScale, + ); + + _paintThumb(context, offset, thumbRect); + } else { + currentThumbRect = null; + } + + for (int index = 0; index < children.length; index += 2) { + _paintChild(context, offset, children[index]); + } + } + + final Paint separatorPaint = Paint(); + + void _paintSeparator( + PaintingContext context, Offset offset, RenderBox child) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + context.paintChild(child, offset + childParentData.offset); + } + + void _paintChild(PaintingContext context, Offset offset, RenderBox child) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + context.paintChild(child, childParentData.offset + offset); + } + + void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) { + // const List thumbShadow = [ + // BoxShadow(color: Color(0x1F000000), offset: Offset(0, 3), blurRadius: 8), + // BoxShadow(color: Color(0x0A000000), offset: Offset(0, 3), blurRadius: 1), + // ]; + + final RRect thumbRRect = + RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius); + + // for (final BoxShadow shadow in thumbShadow) { + // context.canvas + // .drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint()); + // } + + context.canvas.drawRRect( + thumbRRect.inflate(0.5), Paint()..color = const Color(0x0A000000)); + + context.canvas.drawRRect(thumbRRect, Paint()..color = thumbColor); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + RenderBox? child = lastChild; + while (child != null) { + final _SegmentedControlContainerBoxParentData childParentData = + child.parentData! as _SegmentedControlContainerBoxParentData; + if ((childParentData.offset & child.size).contains(position)) { + return result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset localOffset) { + assert(localOffset == position - childParentData.offset); + return child!.hitTest(result, position: localOffset); + }, + ); + } + child = childParentData.previousSibling; + } + return false; + } +} diff --git a/lib/widgets/text.dart b/lib/widgets/text.dart index fe7bbd4..495f3a4 100644 --- a/lib/widgets/text.dart +++ b/lib/widgets/text.dart @@ -84,6 +84,7 @@ class EmojiText extends StatelessWidget { @override Widget build(BuildContext context) { return RichText( + textScaler: MediaQuery.of(context).textScaler, maxLines: maxLines, overflow: overflow ?? TextOverflow.clip, text: TextSpan( diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 3c862a5..7e82934 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -30,3 +30,6 @@ export 'scroll.dart'; export 'dialog.dart'; export 'effect.dart'; export 'palette.dart'; +export 'tab.dart'; +export 'container.dart'; +export 'notification.dart'; diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml index 6f44d7d..d67a90d 100644 --- a/linux/packaging/appimage/make_config.yaml +++ b/linux/packaging/appimage/make_config.yaml @@ -10,7 +10,6 @@ keywords: generic_name: FlClash - categories: - Network diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index 86879ee..535ae37 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -10,6 +10,9 @@ installed_size: 6604 essential: false icon: ./assets/images/icon.png +dependencies: + - libayatana-appindicator3-dev + - libkeybinder-3.0-dev keywords: - FlClash diff --git a/plugins/flutter_distributor b/plugins/flutter_distributor index c5c06ee..9daab58 160000 --- a/plugins/flutter_distributor +++ b/plugins/flutter_distributor @@ -1 +1 @@ -Subproject commit c5c06ee67d8eeee94c0cf2429fe9fe5bb4180bc2 +Subproject commit 9daab581b09afa187bbb0af6c0c1f9a78cd12937 diff --git a/pubspec.lock b/pubspec.lock index f84e560..ceeea69 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -279,7 +279,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: transitive + dependency: "direct dev" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -843,7 +843,7 @@ packages: source: hosted version: "0.12.17" material_color_utilities: - dependency: transitive + dependency: "direct main" description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec diff --git a/pubspec.yaml b/pubspec.yaml index b410656..eaaf656 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.82+202504182 +version: 0.8.83+202505011 environment: sdk: '>=3.1.0 <4.0.0' @@ -53,6 +53,7 @@ dependencies: flutter_riverpod: ^2.6.1 riverpod_annotation: ^2.6.1 riverpod: ^2.6.1 + material_color_utilities: ^0.11.1 dev_dependencies: flutter_test: sdk: flutter @@ -65,6 +66,7 @@ dev_dependencies: riverpod_generator: ^2.6.3 custom_lint: ^0.7.0 riverpod_lint: ^2.6.3 + crypto: ^3.0.3 flutter: uses-material-design: true @@ -92,5 +94,5 @@ ffigen: flutter_intl: enabled: true class_name: AppLocalizations - arb_dir: lib/l10n/arb + arb_dir: arb output_dir: lib/l10n \ No newline at end of file diff --git a/release_telegram.py b/release_telegram.py index af4f159..f4d5883 100644 --- a/release_telegram.py +++ b/release_telegram.py @@ -4,6 +4,7 @@ import requests TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") TAG = os.getenv("TAG") +RUN_ID = os.getenv("RUN_ID") IS_STABLE = "-" not in TAG @@ -45,7 +46,8 @@ if TAG: if IS_STABLE: text += f"\nhttps://github.com/chen08209/FlClash/releases/tag/{TAG}\n" - +else: + text += f"\nhttps://github.com/chen08209/FlClash/actions/runs/{RUN_ID}\n" if os.path.exists(release): text += "\n" diff --git a/services/helper/Cargo.lock b/services/helper/Cargo.lock index 73ced72..b9a53d0 100644 --- a/services/helper/Cargo.lock +++ b/services/helper/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -284,6 +284,7 @@ dependencies = [ "anyhow", "once_cell", "serde", + "sha2", "tokio", "warp", "windows-service", @@ -822,6 +823,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" diff --git a/services/helper/Cargo.toml b/services/helper/Cargo.toml index c738f23..668f1ae 100644 --- a/services/helper/Cargo.toml +++ b/services/helper/Cargo.toml @@ -14,10 +14,11 @@ anyhow = "1.0.93" warp = "0.3.7" serde = { version = "1.0.215", features = ["derive"] } once_cell = "1.20.2" +sha2 = "0.10.8" [profile.release] panic = "abort" codegen-units = 1 lto = true -opt-level = "s" \ No newline at end of file +opt-level = "s" diff --git a/services/helper/build.rs b/services/helper/build.rs new file mode 100644 index 0000000..c612b56 --- /dev/null +++ b/services/helper/build.rs @@ -0,0 +1,4 @@ +fn main() { + let version = std::env::var("TOKEN").unwrap_or_default(); + println!("cargo:rustc-env=TOKEN={}", version); +} diff --git a/services/helper/src/service/hub.rs b/services/helper/src/service/hub.rs index 6286058..f7338e5 100644 --- a/services/helper/src/service/hub.rs +++ b/services/helper/src/service/hub.rs @@ -1,11 +1,13 @@ +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::VecDeque; -use std::{io, thread}; -use std::io::BufRead; +use std::fs::File; +use std::io::{BufRead, Error, Read}; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; +use std::{io, thread}; use warp::{Filter, Reply}; -use serde::{Deserialize, Serialize}; -use once_cell::sync::Lazy; const LISTEN_PORT: u16 = 47890; @@ -15,10 +17,31 @@ pub struct StartParams { pub arg: String, } -static LOGS: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(VecDeque::with_capacity(100)))); -static PROCESS: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); +fn sha256_file(path: &str) -> Result { + let mut file = File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0; 4096]; + + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + + Ok(format!("{:x}", hasher.finalize())) +} + +static LOGS: Lazy>>> = + Lazy::new(|| Arc::new(Mutex::new(VecDeque::with_capacity(100)))); +static PROCESS: Lazy>>> = + Lazy::new(|| Arc::new(Mutex::new(None))); fn start(start_params: StartParams) -> impl Reply { + if sha256_file(start_params.path.as_str()).unwrap_or("".to_string()) != env!("TOKEN") { + return "Only FlClashCore is allowed to run.".to_string(); + } stop(); let mut process = PROCESS.lock().unwrap(); match Command::new(&start_params.path) @@ -73,38 +96,29 @@ fn log_message(message: String) { fn get_logs() -> impl Reply { let log_buffer = LOGS.lock().unwrap(); - let value = log_buffer.iter().cloned().collect::>().join("\n"); + let value = log_buffer + .iter() + .cloned() + .collect::>() + .join("\n"); warp::reply::with_header(value, "Content-Type", "text/plain") } pub async fn run_service() -> anyhow::Result<()> { - let api_ping = warp::get() - .and(warp::path("ping")) - .map(|| "2024125"); + let api_ping = warp::get().and(warp::path("ping")).map(|| env!("TOKEN")); let api_start = warp::post() .and(warp::path("start")) .and(warp::body::json()) - .map(|start_params: StartParams| { - start(start_params) - }); + .map(|start_params: StartParams| start(start_params)); - let api_stop = warp::post() - .and(warp::path("stop")) - .map(|| stop()); + let api_stop = warp::post().and(warp::path("stop")).map(|| stop()); - let api_logs = warp::get() - .and(warp::path("logs")) - .map(|| get_logs()); + let api_logs = warp::get().and(warp::path("logs")).map(|| get_logs()); - warp::serve( - api_ping - .or(api_start) - .or(api_stop) - .or(api_logs) - ) + warp::serve(api_ping.or(api_start).or(api_stop).or(api_logs)) .run(([127, 0, 0, 1], LISTEN_PORT)) .await; Ok(()) -} \ No newline at end of file +} diff --git a/setup.dart b/setup.dart index 2df19f8..8b1f9f8 100755 --- a/setup.dart +++ b/setup.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:path/path.dart'; +import 'package:crypto/crypto.dart'; enum Target { windows, @@ -195,7 +196,16 @@ class Build { if (exitCode != 0 && name != null) throw "$name error"; } - static buildCore({ + static Future calcSha256(String filePath) async { + final file = File(filePath); + if (!await file.exists()) { + throw "File not exists"; + } + final stream = file.openRead(); + return sha256.convert(await stream.reduce((a, b) => a + b)).toString(); + } + + static Future> buildCore({ required Mode mode, required Target target, Arch? arch, @@ -209,6 +219,8 @@ class Build { }, ).toList(); + final List corePaths = []; + for (final item in items) { final outFileDir = join( outDir, @@ -228,6 +240,7 @@ class Build { outFileDir, fileName, ); + corePaths.add(outPath); final Map env = {}; env["GOOS"] = item.target.os; @@ -258,9 +271,11 @@ class Build { workingDirectory: _coreDir, ); } + + return corePaths; } - static buildHelper(Target target) async { + static buildHelper(Target target, String token) async { await exec( [ "cargo", @@ -269,6 +284,9 @@ class Build { "--features", "windows-service", ], + environment: { + "TOKEN": token, + }, name: "build helper", workingDirectory: _servicesDir, ); @@ -278,13 +296,15 @@ class Build { "release", "helper${target.executableExtensionName}", ); - final targetPath = join(outDir, target.name, - "FlClashHelperService${target.executableExtensionName}"); + final targetPath = join( + outDir, + target.name, + "FlClashHelperService${target.executableExtensionName}", + ); await File(outPath).copy(targetPath); } static List getExecutable(String command) { - print(command); return command.split(" "); } @@ -402,7 +422,8 @@ class BuildCommand extends Command { await Build.exec( Build.getExecutable("sudo apt install -y libfuse2"), ); - final downloadName = arch == Arch.amd64 ? "x86_64" : "aarch_64"; + + final downloadName = arch == Arch.amd64 ? "x86_64" : "aarch64"; await Build.exec( Build.getExecutable( "wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage", @@ -413,12 +434,12 @@ class BuildCommand extends Command { "chmod +x appimagetool", ), ); + await Build.exec( + Build.getExecutable( + "sudo mv appimagetool /usr/local/bin/", + ), + ); } - await Build.exec( - Build.getExecutable( - "sudo mv appimagetool /usr/local/bin/", - ), - ); } _getMacosDependencies() async { @@ -466,26 +487,27 @@ class BuildCommand extends Command { throw "Invalid arch parameter"; } - await Build.buildCore( + final corePaths = await Build.buildCore( target: target, arch: arch, mode: mode, ); - if (target == Target.windows) { - await Build.buildHelper(target); - } - if (out != "app") { return; } switch (target) { case Target.windows: + final token = target != Target.android + ? await Build.calcSha256(corePaths.first) + : null; + Build.buildHelper(target, token!); _buildDistributor( target: target, targets: "exe,zip", - args: " --description $archName", + args: + " --description $archName --build-dart-define=CORE_SHA256=$token", env: env, ); return; @@ -496,10 +518,8 @@ class BuildCommand extends Command { }; final targets = [ "deb", - if (arch == Arch.amd64) ...[ - "appimage", - "rpm", - ], + if (arch == Arch.amd64) "appimage", + if (arch == Arch.amd64) "rpm", ].join(","); final defaultTarget = targetMap[arch]; await _getLinuxDependencies(arch!); diff --git a/windows/packaging/exe/inno_setup.iss b/windows/packaging/exe/inno_setup.iss new file mode 100644 index 0000000..bbd56d0 --- /dev/null +++ b/windows/packaging/exe/inno_setup.iss @@ -0,0 +1,83 @@ +[Setup] +AppId={{APP_ID}} +AppVersion={{APP_VERSION}} +AppName={{DISPLAY_NAME}} +AppPublisher={{PUBLISHER_NAME}} +AppPublisherURL={{PUBLISHER_URL}} +AppSupportURL={{PUBLISHER_URL}} +AppUpdatesURL={{PUBLISHER_URL}} +DefaultDirName={{INSTALL_DIR_NAME}} +DisableProgramGroupPage=yes +OutputDir=. +OutputBaseFilename={{OUTPUT_BASE_FILENAME}} +Compression=lzma +SolidCompression=yes +SetupIconFile={{SETUP_ICON_FILE}} +WizardStyle=modern +PrivilegesRequired={{PRIVILEGES_REQUIRED}} +ArchitecturesAllowed={{ARCH}} +ArchitecturesInstallIn64BitMode={{ARCH}} + +[Code] +procedure KillProcesses; +var + Processes: TArrayOfString; + i: Integer; + ResultCode: Integer; +begin + Processes := ['FlClash.exe', 'FlClashCore.exe', 'FlClashHelperService.exe']; + + for i := 0 to GetArrayLength(Processes)-1 do + begin + Exec('taskkill', '/f /im ' + Processes[i], '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; +end; + +function InitializeSetup(): Boolean; +begin + KillProcesses; + Result := True; +end; + +[Languages] +{% for locale in LOCALES %} +{% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} +{% if locale.lang == 'hy' %}Name: "armenian"; MessagesFile: "compiler:Languages\\Armenian.isl"{% endif %} +{% if locale.lang == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %} +{% if locale.lang == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %} +{% if locale.lang == 'zh' %} +Name: "chineseSimplified"; MessagesFile: {% if locale.file %}{{ locale.file }}{% else %}"compiler:Languages\\ChineseSimplified.isl"{% endif %} +{% endif %} +{% if locale.lang == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %} +{% if locale.lang == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %} +{% if locale.lang == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %} +{% if locale.lang == 'nl' %}Name: "dutch"; MessagesFile: "compiler:Languages\\Dutch.isl"{% endif %} +{% if locale.lang == 'fi' %}Name: "finnish"; MessagesFile: "compiler:Languages\\Finnish.isl"{% endif %} +{% if locale.lang == 'fr' %}Name: "french"; MessagesFile: "compiler:Languages\\French.isl"{% endif %} +{% if locale.lang == 'de' %}Name: "german"; MessagesFile: "compiler:Languages\\German.isl"{% endif %} +{% if locale.lang == 'he' %}Name: "hebrew"; MessagesFile: "compiler:Languages\\Hebrew.isl"{% endif %} +{% if locale.lang == 'is' %}Name: "icelandic"; MessagesFile: "compiler:Languages\\Icelandic.isl"{% endif %} +{% if locale.lang == 'it' %}Name: "italian"; MessagesFile: "compiler:Languages\\Italian.isl"{% endif %} +{% if locale.lang == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %} +{% if locale.lang == 'no' %}Name: "norwegian"; MessagesFile: "compiler:Languages\\Norwegian.isl"{% endif %} +{% if locale.lang == 'pl' %}Name: "polish"; MessagesFile: "compiler:Languages\\Polish.isl"{% endif %} +{% if locale.lang == 'pt' %}Name: "portuguese"; MessagesFile: "compiler:Languages\\Portuguese.isl"{% endif %} +{% if locale.lang == 'ru' %}Name: "russian"; MessagesFile: "compiler:Languages\\Russian.isl"{% endif %} +{% if locale.lang == 'sk' %}Name: "slovak"; MessagesFile: "compiler:Languages\\Slovak.isl"{% endif %} +{% if locale.lang == 'sl' %}Name: "slovenian"; MessagesFile: "compiler:Languages\\Slovenian.isl"{% endif %} +{% if locale.lang == 'es' %}Name: "spanish"; MessagesFile: "compiler:Languages\\Spanish.isl"{% endif %} +{% if locale.lang == 'tr' %}Name: "turkish"; MessagesFile: "compiler:Languages\\Turkish.isl"{% endif %} +{% if locale.lang == 'uk' %}Name: "ukrainian"; MessagesFile: "compiler:Languages\\Ukrainian.isl"{% endif %} +{% endfor %} + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %} +[Files] +Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" +Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon +[Run] +Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent \ No newline at end of file diff --git a/windows/packaging/exe/make_config.yaml b/windows/packaging/exe/make_config.yaml index 8d17356..20593be 100644 --- a/windows/packaging/exe/make_config.yaml +++ b/windows/packaging/exe/make_config.yaml @@ -1,3 +1,4 @@ +script_template: inno_setup.iss app_id: 728B3532-C74B-4870-9068-BE70FE12A3E6 app_name: FlClash publisher: chen08209 @@ -9,4 +10,5 @@ setup_icon_file: ..\windows\runner\resources\app_icon.ico locales: - lang: zh file: ..\windows\packaging\exe\ChineseSimplified.isl - - lang: en \ No newline at end of file + - lang: en +privileges_required: admin \ No newline at end of file