From 676f2d058a259d3ba62d9d4a4eff0224f89f0dbb Mon Sep 17 00:00:00 2001 From: chen08209 Date: Fri, 18 Apr 2025 17:50:46 +0800 Subject: [PATCH] Add windows server mode start process verify Add linux deb dependencies Add backup recovery strategy select Support custom text scaling Optimize the display of different text scale Optimize windows setup experience Optimize startTun performance Optimize android tv experience Optimize default option Optimize computed text size Optimize hyperOS freeform window Add developer mode Update core Optimize more details --- .github/workflows/build.yaml | 11 +- README.md | 4 +- README_zh_CN.md | 4 +- android/app/src/main/AndroidManifest.xml | 1 + .../kotlin/com/follow/clash/GlobalState.kt | 4 + .../kotlin/com/follow/clash/MainActivity.kt | 1 - .../kotlin/com/follow/clash/extensions/Ext.kt | 1 - .../kotlin/com/follow/clash/models/Package.kt | 3 +- .../com/follow/clash/plugins/AppPlugin.kt | 20 +- .../com/follow/clash/plugins/VpnPlugin.kt | 4 +- .../clash/services/BaseServiceInterface.kt | 86 ++ .../follow/clash/services/FlClashService.kt | 135 +- .../clash/services/FlClashVpnService.kt | 135 +- android/core/build.gradle.kts | 20 +- android/core/src/main/cpp/CMakeLists.txt | 2 + {lib/l10n/arb => arb}/intl_en.arb | 17 +- {lib/l10n/arb => arb}/intl_ja.arb | 18 +- {lib/l10n/arb => arb}/intl_ru.arb | 18 +- {lib/l10n/arb => arb}/intl_zh_CN.arb | 18 +- core/Clash.Meta | 2 +- core/action.go | 3 + core/common.go | 1 - core/constant.go | 1 + core/go.mod | 13 +- core/go.sum | 22 +- core/hub.go | 4 + core/lib_android.go | 19 +- core/state/state.go | 1 - lib/application.dart | 91 +- lib/clash/interface.dart | 10 + lib/clash/service.dart | 13 +- lib/common/common.dart | 2 +- lib/common/constant.dart | 26 +- lib/common/context.dart | 36 +- lib/common/fixed.dart | 79 ++ lib/common/iterable.dart | 41 +- lib/common/list.dart | 93 -- lib/common/measure.dart | 130 +- lib/common/navigation.dart | 1 - lib/common/num.dart | 5 + lib/common/print.dart | 6 +- lib/common/request.dart | 2 +- lib/common/system.dart | 8 +- lib/common/theme.dart | 25 +- lib/common/tray.dart | 2 +- lib/controller.dart | 98 +- lib/enum/enum.dart | 37 +- lib/fragments/about.dart | 118 +- lib/fragments/access.dart | 91 +- lib/fragments/backup_and_recovery.dart | 46 + lib/fragments/config/network.dart | 11 +- lib/fragments/connection/requests.dart | 203 +-- lib/fragments/dashboard/dashboard.dart | 6 +- .../dashboard/widgets/memory_info.dart | 69 +- .../dashboard/widgets/network_detection.dart | 2 +- .../dashboard/widgets/outbound_mode.dart | 184 ++- .../dashboard/widgets/quick_options.dart | 84 ++ .../dashboard/widgets/start_button.dart | 24 +- lib/fragments/developer.dart | 120 ++ lib/fragments/fragments.dart | 1 + lib/fragments/logs.dart | 233 ++-- lib/fragments/profiles/add_profile.dart | 7 +- lib/fragments/profiles/edit_profile.dart | 4 + lib/fragments/profiles/override_profile.dart | 23 +- lib/fragments/profiles/profiles.dart | 2 +- lib/fragments/proxies/common.dart | 2 +- lib/fragments/proxies/list.dart | 57 +- lib/fragments/proxies/proxies.dart | 11 +- lib/fragments/resources.dart | 27 +- lib/fragments/theme.dart | 547 +++++--- lib/fragments/tools.dart | 28 +- lib/l10n/intl/messages_en.dart | 25 + lib/l10n/intl/messages_ja.dart | 19 + lib/l10n/intl/messages_ru.dart | 25 + lib/l10n/intl/messages_zh_CN.dart | 15 + lib/l10n/l10n.dart | 115 ++ lib/main.dart | 4 + lib/manager/app_state_manager.dart | 15 +- lib/manager/clash_manager.dart | 8 +- lib/manager/message_manager.dart | 73 +- lib/manager/theme_manager.dart | 27 +- lib/manager/window_manager.dart | 21 +- lib/models/app.dart | 1 - lib/models/clash_config.dart | 3 +- lib/models/common.dart | 68 +- lib/models/config.dart | 42 +- lib/models/generated/app.freezed.dart | 81 +- .../generated/clash_config.freezed.dart | 29 +- lib/models/generated/clash_config.g.dart | 5 +- lib/models/generated/common.freezed.dart | 579 ++++++++- lib/models/generated/common.g.dart | 66 +- lib/models/generated/config.freezed.dart | 333 ++++- lib/models/generated/config.g.dart | 43 +- lib/models/generated/core.g.dart | 1 + lib/models/generated/selector.freezed.dart | 228 +++- lib/models/selector.dart | 29 +- lib/pages/editor.dart | 3 +- lib/pages/home.dart | 85 +- lib/providers/app.dart | 15 - lib/providers/generated/app.g.dart | 15 - lib/providers/generated/state.g.dart | 48 +- lib/providers/state.dart | 39 +- lib/state.dart | 31 +- lib/widgets/color_scheme_box.dart | 2 +- lib/widgets/container.dart | 73 ++ lib/widgets/donut_chart.dart | 2 +- lib/widgets/fade_box.dart | 3 + lib/widgets/list.dart | 101 +- lib/widgets/notification.dart | 33 + lib/widgets/scaffold.dart | 238 ++-- lib/widgets/scroll.dart | 71 +- lib/widgets/super_grid.dart | 45 +- lib/widgets/tab.dart | 1100 +++++++++++++++++ lib/widgets/text.dart | 1 + lib/widgets/widgets.dart | 3 + linux/packaging/appimage/make_config.yaml | 1 - linux/packaging/deb/make_config.yaml | 3 + plugins/flutter_distributor | 2 +- pubspec.lock | 4 +- pubspec.yaml | 6 +- release_telegram.py | 4 +- services/helper/Cargo.lock | 14 +- services/helper/Cargo.toml | 3 +- services/helper/build.rs | 4 + services/helper/src/service/hub.rs | 66 +- setup.dart | 62 +- windows/packaging/exe/inno_setup.iss | 83 ++ windows/packaging/exe/make_config.yaml | 4 +- 128 files changed, 5455 insertions(+), 1737 deletions(-) rename {lib/l10n/arb => arb}/intl_en.arb (96%) rename {lib/l10n/arb => arb}/intl_ja.arb (95%) rename {lib/l10n/arb => arb}/intl_ru.arb (96%) rename {lib/l10n/arb => arb}/intl_zh_CN.arb (95%) create mode 100644 lib/common/fixed.dart delete mode 100644 lib/common/list.dart create mode 100644 lib/fragments/developer.dart create mode 100644 lib/widgets/container.dart create mode 100644 lib/widgets/notification.dart create mode 100644 lib/widgets/tab.dart create mode 100644 services/helper/build.rs create mode 100644 windows/packaging/exe/inno_setup.iss 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