From ef5f6dbd5912349249640cddfd81a439f5193fa6 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Wed, 12 Mar 2025 17:15:31 +0800 Subject: [PATCH] Add rule override Update core Optimize more details --- .github/workflows/build.yaml | 21 +- .gitmodules | 2 +- analysis_options.yaml | 7 - android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 15 +- .../com/follow/clash/FlClashApplication.kt | 2 +- .../kotlin/com/follow/clash/GlobalState.kt | 2 - .../com/follow/clash/plugins/AppPlugin.kt | 68 +- .../com/follow/clash/plugins/ServicePlugin.kt | 1 - .../com/follow/clash/plugins/VpnPlugin.kt | 1 - .../follow/clash/services/FlClashService.kt | 12 +- .../clash/services/FlClashVpnService.kt | 36 +- android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- assets/fonts/JetBrainsMono-Regular.ttf | Bin 0 -> 273900 bytes core/Clash.Meta | 2 +- core/common.go | 48 +- core/constant.go | 1 + core/go.mod | 11 +- core/go.sum | 22 +- lib/application.dart | 60 +- lib/clash/core.dart | 2 +- lib/common/color.dart | 36 +- lib/common/constant.dart | 9 +- lib/common/function.dart | 8 +- lib/common/iterable.dart | 9 + lib/common/list.dart | 8 +- lib/common/measure.dart | 12 +- lib/common/mixin.dart | 9 +- lib/common/navigator.dart | 11 +- lib/common/other.dart | 5 - lib/common/protocol.dart | 8 +- lib/common/render.dart | 6 +- lib/common/request.dart | 20 +- lib/common/scroll.dart | 6 +- lib/common/text.dart | 9 +- lib/common/theme.dart | 39 + lib/common/window.dart | 12 +- lib/controller.dart | 111 +- lib/enum/enum.dart | 67 +- lib/fragments/access.dart | 36 +- lib/fragments/application_setting.dart | 2 +- lib/fragments/backup_and_recovery.dart | 183 ++-- lib/fragments/config/config.dart | 63 +- lib/fragments/config/dns.dart | 86 +- lib/fragments/config/general.dart | 11 +- lib/fragments/config/network.dart | 13 +- lib/fragments/connection/connections.dart | 2 +- lib/fragments/connection/item.dart | 207 ++-- lib/fragments/connection/requests.dart | 48 +- .../dashboard/widgets/intranet_ip.dart | 2 +- .../dashboard/widgets/memory_info.dart | 104 +- .../dashboard/widgets/network_detection.dart | 37 +- .../dashboard/widgets/network_speed.dart | 2 +- .../dashboard/widgets/outbound_mode.dart | 2 +- .../dashboard/widgets/quick_options.dart | 44 +- .../dashboard/widgets/traffic_usage.dart | 6 +- lib/fragments/hotkey.dart | 45 +- lib/fragments/logs.dart | 52 +- lib/fragments/profiles/add_profile.dart | 24 +- lib/fragments/profiles/edit_profile.dart | 12 +- lib/fragments/profiles/gen_profile.dart | 87 -- lib/fragments/profiles/override_profile.dart | 957 ++++++++++++++++++ lib/fragments/profiles/profiles.dart | 222 ++-- lib/fragments/proxies/card.dart | 4 +- lib/fragments/proxies/list.dart | 8 +- lib/fragments/proxies/providers.dart | 46 +- lib/fragments/proxies/proxies.dart | 45 +- lib/fragments/proxies/setting.dart | 5 +- lib/fragments/proxies/tab.dart | 131 +-- lib/fragments/resources.dart | 37 +- lib/fragments/theme.dart | 4 +- lib/fragments/tools.dart | 14 +- lib/l10n/arb/intl_en.arb | 34 +- lib/l10n/arb/intl_ja.arb | 34 +- lib/l10n/arb/intl_ru.arb | 34 +- lib/l10n/arb/intl_zh_CN.arb | 36 +- lib/l10n/intl/messages_en.dart | 60 +- lib/l10n/intl/messages_ja.dart | 38 +- lib/l10n/intl/messages_ru.dart | 60 +- lib/l10n/intl/messages_zh_CN.dart | 38 +- lib/l10n/l10n.dart | 270 ++++- lib/main.dart | 1 + lib/manager/app_state_manager.dart | 24 + lib/manager/manager.dart | 3 +- lib/manager/message_manager.dart | 249 ++--- lib/manager/theme_manager.dart | 38 + lib/manager/window_manager.dart | 4 +- lib/models/app.dart | 4 +- lib/models/clash_config.dart | 140 ++- lib/models/common.dart | 12 +- lib/models/config.dart | 4 +- lib/models/core.dart | 1 + lib/models/generated/app.freezed.dart | 42 +- .../generated/clash_config.freezed.dart | 840 ++++++++++++++- lib/models/generated/clash_config.g.dart | 45 +- lib/models/generated/config.freezed.dart | 2 +- lib/models/generated/config.g.dart | 2 +- lib/models/generated/core.freezed.dart | 35 +- lib/models/generated/core.g.dart | 2 + lib/models/generated/profile.freezed.dart | 433 +++++++- lib/models/generated/profile.g.dart | 44 + lib/models/generated/selector.freezed.dart | 489 ++++++++- lib/models/generated/widget.freezed.dart | 500 +++++++-- lib/models/profile.dart | 49 + lib/models/selector.dart | 33 +- lib/models/widget.dart | 27 +- lib/pages/editor.dart | 358 ++++++- lib/pages/home.dart | 155 +-- lib/pages/scan.dart | 3 +- lib/providers/app.dart | 31 +- lib/providers/config.dart | 9 + lib/providers/generated/app.g.dart | 81 +- lib/providers/generated/config.g.dart | 2 +- lib/providers/generated/state.g.dart | 160 ++- lib/providers/state.dart | 44 +- lib/state.dart | 51 +- lib/widgets/back_scope.dart | 33 - lib/widgets/card.dart | 99 +- lib/widgets/chip.dart | 5 +- lib/widgets/dialog.dart | 50 + lib/widgets/donut_chart.dart | 2 - lib/widgets/effect.dart | 59 ++ lib/widgets/fade_box.dart | 35 +- lib/widgets/input.dart | 574 +++++++---- lib/widgets/line_chart.dart | 5 +- lib/widgets/list.dart | 342 ++++--- lib/widgets/open_container.dart | 1 + lib/widgets/pop_scope.dart | 36 + lib/widgets/popup.dart | 176 ++-- lib/widgets/scaffold.dart | 360 ++++--- lib/widgets/scroll.dart | 24 +- lib/widgets/sheet.dart | 249 +++-- lib/widgets/side_sheet.dart | 64 +- lib/widgets/subscription_info_view.dart | 2 +- lib/widgets/super_grid.dart | 39 +- lib/widgets/widgets.dart | 4 +- macos/Podfile.lock | 4 +- macos/Runner.xcodeproj/project.pbxproj | 24 + macos/Runner/MainFlutterWindow.swift | 44 +- plugins/flutter_distributor | 2 +- pubspec.lock | 74 +- pubspec.yaml | 19 +- release.py => release_telegram.py | 4 +- setup.dart | 16 +- 145 files changed, 7710 insertions(+), 2404 deletions(-) create mode 100644 assets/fonts/JetBrainsMono-Regular.ttf create mode 100644 lib/common/theme.dart delete mode 100644 lib/fragments/profiles/gen_profile.dart create mode 100644 lib/fragments/profiles/override_profile.dart create mode 100644 lib/manager/theme_manager.dart delete mode 100644 lib/widgets/back_scope.dart create mode 100644 lib/widgets/dialog.dart create mode 100644 lib/widgets/effect.dart create mode 100644 lib/widgets/pop_scope.dart rename release.py => release_telegram.py (96%) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5164930..0b8db24 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,6 +4,8 @@ on: push: tags: - 'v*' +env: + IS_STABLE: ${{ !contains(github.ref, '-') }} jobs: build: @@ -74,7 +76,7 @@ jobs: run: flutter pub get - name: Setup - run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} + run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }} - name: Upload uses: actions/upload-artifact@v4 @@ -88,14 +90,13 @@ jobs: needs: [ build ] steps: - name: Checkout - if: ${{ !contains(github.ref, '+') }} uses: actions/checkout@v4 + if: ${{ env.IS_STABLE == 'true' }} with: fetch-depth: 0 ref: refs/heads/main - - name: Generate - if: ${{ !contains(github.ref, '+') }} + if: ${{ env.IS_STABLE == 'true' }} run: | tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1) @@ -127,7 +128,7 @@ jobs: cat NEW_CHANGELOG.md > CHANGELOG.md - name: Commit - if: ${{ !contains(github.ref, '+') }} + if: ${{ env.IS_STABLE == 'true' }} run: | git add CHANGELOG.md if ! git diff --cached --quiet; then @@ -206,7 +207,7 @@ jobs: run: | python -m pip install --upgrade pip pip install requests - python release.py + python release_telegram.py - name: Patch release.md run: | @@ -214,21 +215,21 @@ jobs: sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md - name: Release - if: ${{ !contains(github.ref, '+') }} + if: ${{ env.IS_STABLE == 'true' }} uses: softprops/action-gh-release@v2 with: files: ./dist/* body_path: './release.md' - name: Create Fdroid Source Dir - if: ${{ !contains(github.ref, '+') }} + if: ${{ env.IS_STABLE == 'true' }} run: | mkdir -p ./tmp cp ./dist/*android-arm64-v8a* ./tmp/ || true echo "Files copied successfully" - name: Push to fdroid repo - if: ${{ !contains(github.ref, '+') }} + if: ${{ env.IS_STABLE == 'true' }} uses: cpina/github-action-push-to-another-repository@v1.7.2 env: SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} @@ -238,7 +239,7 @@ jobs: destination-repository-name: FlClash-fdroid-repo user-name: 'github-actions[bot]' user-email: 'github-actions[bot]@users.noreply.github.com' - target-branch: action-pr + target-branch: main commit-message: Update from ${{ github.ref_name }} target-directory: /tmp/ diff --git a/.gitmodules b/.gitmodules index 7577fac..70bf7cd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "core/Clash.Meta"] path = core/Clash.Meta url = git@github.com:chen08209/Clash.Meta.git - branch = FlClash-Alpha + branch = FlClash [submodule "plugins/flutter_distributor"] path = plugins/flutter_distributor url = git@github.com:chen08209/flutter_distributor.git diff --git a/analysis_options.yaml b/analysis_options.yaml index 0fbea86..f9b3034 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,8 +1 @@ include: package:flutter_lints/flutter.yaml - -linter: - rules: - -analyzer: - plugins: - - custom_lint diff --git a/android/app/build.gradle b/android/app/build.gradle index fdf1998..bc8d1d0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,7 +33,7 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias android { namespace "com.follow.clash" - compileSdkVersion 34 + compileSdkVersion 35 ndkVersion "27.1.12297006" compileOptions { @@ -63,7 +63,7 @@ android { defaultConfig { applicationId "com.follow.clash" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 185ac9c..606a14a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,14 +10,12 @@ - - + + @@ -64,7 +62,9 @@ - + @@ -138,7 +137,7 @@ + android:foregroundServiceType="dataSync"> diff --git a/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt b/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt index 8326a75..e2b2783 100644 --- a/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt +++ b/android/app/src/main/kotlin/com/follow/clash/FlClashApplication.kt @@ -1,7 +1,7 @@ package com.follow.clash; import android.app.Application -import android.content.Context; +import android.content.Context class FlClashApplication : Application() { companion object { 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 d3fef86..d2b8d4c 100644 --- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -1,9 +1,7 @@ package com.follow.clash -import android.content.Context import androidx.lifecycle.MutableLiveData import com.follow.clash.plugins.AppPlugin -import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.VpnPlugin import io.flutter.FlutterInjector 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 8683291..12bdb39 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 @@ -291,16 +291,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware private fun getPackages(): List { val packageManager = FlClashApplication.getAppContext().packageManager if (packages.isNotEmpty()) return packages - packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { - it.packageName != FlClashApplication.getAppContext().packageName - || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true - || it.packageName == "android" + 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" + ) - }?.map { + }?.map { Package( packageName = it.packageName, - label = it.applicationInfo.loadLabel(packageManager).toString(), - isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, + label = it.applicationInfo?.loadLabel(packageManager).toString(), + isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1, lastUpdateTime = it.lastUpdateTime ) }?.let { packages.addAll(it) } @@ -353,7 +355,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } suspend fun getText(text: String): String? { - return withContext(Dispatchers.Default){ + return withContext(Dispatchers.Default) { channel.awaitResult("getText", text) } } @@ -391,31 +393,33 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware }.forEach { if (it.name.matches(chinaAppRegex)) return true } - ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use { - for (packageEntry in it.entries()) { - if (packageEntry.name.startsWith("firebase-")) return false - } - for (packageEntry in it.entries()) { - if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith( - ".dex" - )) - ) { - continue + packageInfo.applicationInfo?.publicSourceDir?.let { + ZipFile(File(it)).use { + for (packageEntry in it.entries()) { + if (packageEntry.name.startsWith("firebase-")) return false } - if (packageEntry.size > 15000000) { - return true - } - val input = it.getInputStream(packageEntry).buffered() - val dexFile = try { - DexBackedDexFile.fromInputStream(null, input) - } catch (e: Exception) { - return false - } - for (clazz in dexFile.classes) { - val clazzName = - clazz.type.substring(1, clazz.type.length - 1).replace("/", ".") - .replace("$", ".") - if (clazzName.matches(chinaAppRegex)) return true + for (packageEntry in it.entries()) { + if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith( + ".dex" + )) + ) { + continue + } + if (packageEntry.size > 15000000) { + return true + } + val input = it.getInputStream(packageEntry).buffered() + val dexFile = try { + DexBackedDexFile.fromInputStream(null, input) + } catch (e: Exception) { + return false + } + for (clazz in dexFile.classes) { + val clazzName = + clazz.type.substring(1, clazz.type.length - 1).replace("/", ".") + .replace("$", ".") + if (clazzName.matches(chinaAppRegex)) return true + } } } } diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt index 2b768c5..2f7ef69 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt @@ -1,6 +1,5 @@ package com.follow.clash.plugins -import com.follow.clash.FlClashApplication import com.follow.clash.GlobalState import com.follow.clash.models.VpnOptions import com.google.gson.Gson 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 a5f2ffd..c4f6e7e 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 @@ -10,7 +10,6 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build import android.os.IBinder -import android.util.Log import androidx.core.content.getSystemService import com.follow.clash.FlClashApplication import com.follow.clash.GlobalState 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 d46a9e3..fa0ba54 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 @@ -7,7 +7,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Binder import android.os.Build import android.os.IBinder @@ -87,6 +87,7 @@ class FlClashService : Service(), BaseServiceInterface { } } } + private suspend fun getNotificationBuilder(): NotificationCompat.Builder { return notificationBuilderDeferred.await() } @@ -100,7 +101,8 @@ class FlClashService : Service(), BaseServiceInterface { } } - @SuppressLint("ForegroundServiceType", "WrongConstant") + + @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) @@ -116,7 +118,11 @@ class FlClashService : Service(), BaseServiceInterface { .setContentTitle(title) .setContentText(content).build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + try { + startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } catch (_: Exception) { + startForeground(notificationId, notification) + } } else { startForeground(notificationId, notification) } 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 7f097ae..37d39a0 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 @@ -6,7 +6,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.net.ProxyInfo import android.net.VpnService import android.os.Binder @@ -45,9 +45,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { addAddress(cidr.address, cidr.prefixLength) val routeAddress = options.getIpv4RouteAddress() if (routeAddress.isNotEmpty()) { - routeAddress.forEach { i -> - Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}") - addRoute(i.address, i.prefixLength) + try { + routeAddress.forEach { i -> + Log.d( + "addRoute4", + "address: ${i.address} prefixLength:${i.prefixLength}" + ) + addRoute(i.address, i.prefixLength) + } + } catch (_: Exception) { + addRoute("0.0.0.0", 0) } } else { addRoute("0.0.0.0", 0) @@ -58,9 +65,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { addAddress(cidr.address, cidr.prefixLength) val routeAddress = options.getIpv6RouteAddress() if (routeAddress.isNotEmpty()) { - routeAddress.forEach { i -> - Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}") - addRoute(i.address, i.prefixLength) + try { + routeAddress.forEach { i -> + Log.d( + "addRoute6", + "address: ${i.address} prefixLength:${i.prefixLength}" + ) + addRoute(i.address, i.prefixLength) + } + } catch (_: Exception) { + addRoute("::", 0) } } else { addRoute("::", 0) @@ -165,7 +179,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { return notificationBuilderDeferred.await() } - @SuppressLint("ForegroundServiceType", "WrongConstant") + @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) @@ -182,7 +196,11 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { .setContentText(content) .build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + try { + startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } catch (_: Exception) { + startForeground(notificationId, notification) + } } else { startForeground(notificationId, notification) } diff --git a/android/gradle.properties b/android/gradle.properties index 74e2e7b..f1c93ba 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true kotlin_version=1.9.22 -agp_version=8.2.1 +agp_version=8.9.1 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index d055c79..90df0e8 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/assets/fonts/JetBrainsMono-Regular.ttf b/assets/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dff66cc50702c75abd025dcf49f62a4dcc2d72de GIT binary patch literal 273900 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 z

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 1 { return parts } @@ -168,18 +172,18 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig return prof } -func genHosts(hosts, patchHosts map[string]any) { +func attachHosts(hosts, patchHosts map[string]any) { for k, v := range patchHosts { if str, ok := v.(string); ok { - hosts[k] = splitByComma(str) + hosts[k] = splitByMultipleSeparators(str) } } } -func modPatchDns(dns *config.RawDNS) { +func updatePatchDns(dns config.RawDNS) { for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() { if str, ok := pair.Value.(string); ok { - dns.NameServerPolicy.Set(pair.Key, splitByComma(str)) + dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str)) } } } @@ -191,26 +195,25 @@ func trimArr(arr []string) (r []string) { return } -func overrideRules(rules *[]string) { - var target = "" - for _, line := range *rules { +func overrideRules(rules, patchRules []string) []string { + target := "" + for _, line := range rules { rule := trimArr(strings.Split(line, ",")) - l := len(rule) - if l != 2 { - return + if len(rule) != 2 { + continue } - if strings.ToUpper(rule[0]) == "MATCH" { + if strings.EqualFold(rule[0], "MATCH") { target = rule[1] break } } if target == "" { - return + return rules } - var rulesExt = lo.Map(ips, func(ip string, index int) string { - return fmt.Sprintf("DOMAIN %s %s", ip, target) + rulesExt := lo.Map(ips, func(ip string, _ int) string { + return fmt.Sprintf("DOMAIN,%s,%s", ip, target) }) - *rules = append(rulesExt, *rules...) + return append(append(rulesExt, patchRules...), rules...) } func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) { @@ -244,16 +247,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi for idx := range targetConfig.ProxyGroup { targetConfig.ProxyGroup[idx]["url"] = "" } - genHosts(targetConfig.Hosts, patchConfig.Hosts) + attachHosts(targetConfig.Hosts, patchConfig.Hosts) if configParams.OverrideDns { - modPatchDns(&patchConfig.DNS) + updatePatchDns(patchConfig.DNS) targetConfig.DNS = patchConfig.DNS } else { if targetConfig.DNS.Enable == false { targetConfig.DNS.Enable = true } } - overrideRules(&targetConfig.Rule) + if configParams.OverrideRule { + targetConfig.Rule = overrideRules(patchConfig.Rule, []string{}) + } else { + targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule) + } } func patchConfig() { @@ -267,6 +274,7 @@ func patchConfig() { dialer.DefaultInterface.Store(general.Interface) adapter.UnifiedDelay.Store(general.UnifiedDelay) tunnel.SetMode(general.Mode) + tunnel.UpdateRules(currentConfig.Rules, currentConfig.SubRules, currentConfig.RuleProviders) log.SetLevel(general.LogLevel) resolver.DisableIPv6 = !general.IPv6 diff --git a/core/constant.go b/core/constant.go index a9355f6..a4cb3f7 100644 --- a/core/constant.go +++ b/core/constant.go @@ -13,6 +13,7 @@ type ConfigExtendedParams struct { SelectedMap map[string]string `json:"selected-map"` TestURL *string `json:"test-url"` OverrideDns bool `json:"override-dns"` + OverrideRule bool `json:"override-rule"` } type GenerateConfigParams struct { diff --git a/core/go.mod b/core/go.mod index 30037f0..aeb042f 100644 --- a/core/go.mod +++ b/core/go.mod @@ -21,7 +21,7 @@ require ( github.com/coreos/go-iptables v0.8.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/ebitengine/purego v0.8.2 // indirect - github.com/enfein/mieru/v3 v3.11.2 // indirect + github.com/enfein/mieru/v3 v3.13.0 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect @@ -50,21 +50,22 @@ require ( github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect 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/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect - github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // 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-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect - github.com/metacubex/sing-tun v0.4.5 // indirect + github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // 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.6 // indirect + github.com/metacubex/utls v1.6.8-alpha.4 // 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 @@ -74,7 +75,7 @@ require ( github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/cors v1.2.1 // indirect diff --git a/core/go.sum b/core/go.sum index 63b60b7..4a7fccb 100644 --- a/core/go.sum +++ b/core/go.sum @@ -28,8 +28,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo= -github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw= +github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98= +github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= @@ -97,14 +97,16 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= +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/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-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= -github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic= +github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= +github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds= 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= @@ -117,16 +119,16 @@ github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJ 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.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= -github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= +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-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.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= -github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= +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/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= @@ -153,8 +155,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= -github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= diff --git a/lib/application.dart b/lib/application.dart index 6cf0f19..416531a 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -61,16 +61,6 @@ class ApplicationState extends ConsumerState { _autoUpdateGroupTask(); _autoUpdateProfilesTask(); globalState.appController = AppController(context, ref); - globalState.measure = Measure.of(context); - // ref.listenManual(themeSettingProvider.select((state) => state.fontFamily), - // (prev, next) { - // if (prev != next) { - // globalState.measure = Measure.of( - // context, - // fontFamily: next.value, - // ); - // } - // }, fireImmediately: true); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final currentContext = globalState.navigatorKey.currentContext; if (currentContext != null) { @@ -98,7 +88,7 @@ class ApplicationState extends ConsumerState { }); } - _buildPlatformWrap(Widget child) { + _buildPlatformState(Widget child) { if (system.isDesktop) { return WindowManager( child: TrayManager( @@ -117,18 +107,7 @@ class ApplicationState extends ConsumerState { ); } - _buildPage(Widget page) { - if (system.isDesktop) { - return WindowHeaderContainer( - child: page, - ); - } - return VpnManager( - child: page, - ); - } - - _buildWrap(Widget child) { + _buildState(Widget child) { return AppStateManager( child: ClashManager( child: ConnectivityManager( @@ -142,6 +121,25 @@ class ApplicationState extends ConsumerState { ); } + _buildPlatformApp(Widget child) { + if (system.isDesktop) { + return WindowHeaderContainer( + child: child, + ); + } + return VpnManager( + child: child, + ); + } + + _buildApp(Widget child) { + return MessageManager( + child: ThemeManager( + child: child, + ), + ); + } + _updateSystemColorSchemes( ColorScheme? lightDynamic, ColorScheme? darkDynamic, @@ -157,8 +155,8 @@ class ApplicationState extends ConsumerState { @override Widget build(context) { - return _buildPlatformWrap( - _buildWrap( + return _buildPlatformState( + _buildState( Consumer( builder: (_, ref, child) { final locale = @@ -168,6 +166,7 @@ class ApplicationState extends ConsumerState { builder: (lightDynamic, darkDynamic) { _updateSystemColorSchemes(lightDynamic, darkDynamic); return MaterialApp( + debugShowCheckedModeBanner: false, navigatorKey: globalState.navigatorKey, localizationsDelegates: const [ AppLocalizations.delegate, @@ -176,14 +175,9 @@ class ApplicationState extends ConsumerState { GlobalWidgetsLocalizations.delegate ], builder: (_, child) { - return MessageManager( - child: LayoutBuilder( - builder: (_, container) { - globalState.appController.updateViewWidth( - container.maxWidth, - ); - return _buildPage(child!); - }, + return AppEnvManager( + child: _buildPlatformApp( + _buildApp(child!), ), ); }, diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 7ea6269..b5ae232 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -240,7 +240,7 @@ class ClashCore { if (res.isEmpty) { return null; } - return ClashConfigSnippet.fromJson(json.decode(res)); + return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res))); } resetTraffic() { diff --git a/lib/common/color.dart b/lib/common/color.dart index 010450f..f81b55e 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -1,20 +1,40 @@ import 'package:flutter/material.dart'; extension ColorExtension on Color { - Color get toLight { - return withOpacity(0.8); + Color get opacity80 { + return withAlpha(204); } - Color get toLighter { - return withOpacity(0.6); + Color get opacity60 { + return withAlpha(153); } - Color get toSoft { - return withOpacity(0.15); + Color get opacity50 { + return withAlpha(128); } - Color get toLittle { - return withOpacity(0.03); + Color get opacity38 { + return withAlpha(97); + } + + Color get opacity30 { + return withAlpha(77); + } + + Color get opacity15 { + return withAlpha(38); + } + + Color get opacity10 { + return withAlpha(15); + } + + Color get opacity3 { + return withAlpha(76); + } + + Color get opacity0 { + return withAlpha(0); } Color darken([double amount = .1]) { diff --git a/lib/common/constant.dart b/lib/common/constant.dart index d751c6c..a71dfc3 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -21,6 +21,11 @@ const baseInfoEdgeInsets = EdgeInsets.symmetric( vertical: 16, horizontal: 16, ); + +double textScaleFactor = min( + WidgetsBinding.instance.platformDispatcher.textScaleFactor, + 1.2, +); const httpTimeoutDuration = Duration(milliseconds: 5000); const moreDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100); @@ -46,7 +51,7 @@ const defaultExternalController = "127.0.0.1:9090"; const maxMobileWidth = 600; const maxLaptopWidth = 840; const defaultTestUrl = "https://www.gstatic.com/generate_204"; -final filter = ImageFilter.blur( +final commonFilter = ImageFilter.blur( sigmaX: 5, sigmaY: 5, tileMode: TileMode.mirror, @@ -76,7 +81,7 @@ const viewModeColumnsMap = { const defaultPrimaryColor = Colors.brown; double getWidgetHeight(num lines) { - return max(lines * 84 + (lines - 1) * 16, 0); + return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0); } final mainIsolate = "FlClashMainIsolate"; diff --git a/lib/common/function.dart b/lib/common/function.dart index 8fd8a9f..a1e1770 100644 --- a/lib/common/function.dart +++ b/lib/common/function.dart @@ -1,7 +1,7 @@ import 'dart:async'; class Debouncer { - final Map _operations = {}; + final Map _operations = {}; call( dynamic tag, @@ -28,14 +28,15 @@ class Debouncer { cancel(dynamic tag) { _operations[tag]?.cancel(); + _operations[tag] = null; } } class Throttler { - final Map _operations = {}; + final Map _operations = {}; call( - String tag, + dynamic tag, Function func, { List? args, Duration duration = const Duration(milliseconds: 600), @@ -60,6 +61,7 @@ class Throttler { cancel(dynamic tag) { _operations[tag]?.cancel(); + _operations[tag] = null; } } diff --git a/lib/common/iterable.dart b/lib/common/iterable.dart index 802423c..aac4471 100644 --- a/lib/common/iterable.dart +++ b/lib/common/iterable.dart @@ -65,3 +65,12 @@ extension DoubleListExt on List { return -1; } } + +extension MapExt on Map { + getCacheValue(K key, V defaultValue) { + if (this[key] == null) { + this[key] = defaultValue; + } + return this[key]; + } +} diff --git a/lib/common/list.dart b/lib/common/list.dart index 531d56a..ca8add7 100644 --- a/lib/common/list.dart +++ b/lib/common/list.dart @@ -32,7 +32,7 @@ class FixedList { } class FixedMap { - final int maxSize; + int maxSize; final Map _map = {}; final Queue _queue = Queue(); @@ -45,6 +45,7 @@ class FixedMap { } _map[key] = value; _queue.add(key); + return value; } clear() { @@ -52,8 +53,13 @@ class FixedMap { _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; diff --git a/lib/common/measure.dart b/lib/common/measure.dart index 0415d95..736fdec 100644 --- a/lib/common/measure.dart +++ b/lib/common/measure.dart @@ -5,13 +5,11 @@ import 'package:flutter/material.dart'; class Measure { final TextScaler _textScale; final BuildContext context; - final String? _fontFamily; - Measure.of(this.context, {String? fontFamily}) + Measure.of(this.context) : _textScale = TextScaler.linear( - WidgetsBinding.instance.platformDispatcher.textScaleFactor, - ), - _fontFamily = fontFamily ?? ""; + textScaleFactor, + ); Size computeTextSize( Text text, { @@ -20,9 +18,7 @@ class Measure { final textPainter = TextPainter( text: TextSpan( text: text.data, - style: text.style?.copyWith( - fontFamily: _fontFamily, - ), + style: text.style, ), maxLines: text.maxLines, textScaler: _textScale, diff --git a/lib/common/mixin.dart b/lib/common/mixin.dart index e29efbb..7fe72fb 100644 --- a/lib/common/mixin.dart +++ b/lib/common/mixin.dart @@ -1,3 +1,4 @@ +import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; import 'package:riverpod/riverpod.dart'; import 'context.dart'; @@ -29,8 +30,14 @@ mixin PageMixin on State { final commonScaffoldState = context.commonScaffoldState; commonScaffoldState?.actions = actions; commonScaffoldState?.floatingActionButton = floatingActionButton; - commonScaffoldState?.onSearch = onSearch; commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate; + commonScaffoldState?.updateSearchState( + (_) => onSearch != null + ? AppBarSearchState( + onSearch: onSearch!, + ) + : null, + ); }); } diff --git a/lib/common/navigator.dart b/lib/common/navigator.dart index b927c34..60e13e5 100644 --- a/lib/common/navigator.dart +++ b/lib/common/navigator.dart @@ -70,7 +70,7 @@ class CommonRoute extends MaterialPageRoute { Duration get transitionDuration => const Duration(milliseconds: 500); @override - Duration get reverseTransitionDuration => const Duration(milliseconds: 250); + Duration get reverseTransitionDuration => const Duration(milliseconds: 500); } final Animatable _kRightMiddleTween = Tween( @@ -194,7 +194,7 @@ class _CommonPageTransitionState extends State { _primaryPositionCurve = CurvedAnimation( parent: widget.primaryRouteAnimation, curve: Curves.fastEaseInToSlowEaseOut, - reverseCurve: Curves.easeInOut, + reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped, ); _secondaryPositionCurve = CurvedAnimation( parent: widget.secondaryRouteAnimation, @@ -218,9 +218,8 @@ class _CommonPageTransitionState extends State { begin: const _CommonEdgeShadowDecoration(), end: _CommonEdgeShadowDecoration( [ - widget.context.colorScheme.inverseSurface.withOpacity( - 0.06, - ), + widget.context.colorScheme.inverseSurface + .withValues(alpha: 0.02), Colors.transparent, ], ), @@ -274,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter { return; } - final double shadowWidth = 0.03 * configuration.size!.width; + final double shadowWidth = 1 * configuration.size!.width; final double shadowHeight = configuration.size!.height; final double bandWidth = shadowWidth / (colors.length - 1); diff --git a/lib/common/other.dart b/lib/common/other.dart index 4be0653..3bdfaad 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -241,11 +241,6 @@ class Other { return "${appName}_${DateTime.now().show}.log"; } - Size getScreenSize() { - final view = WidgetsBinding.instance.platformDispatcher.views.first; - return view.physicalSize / view.devicePixelRatio; - } - Future getLocalIpAddress() async { List interfaces = await NetworkInterface.list( includeLoopback: false, diff --git a/lib/common/protocol.dart b/lib/common/protocol.dart index a4fedf2..129fb73 100644 --- a/lib/common/protocol.dart +++ b/lib/common/protocol.dart @@ -14,15 +14,13 @@ class Protocol { void register(String scheme) { String protocolRegKey = 'Software\\Classes\\$scheme'; - RegistryValue protocolRegValue = const RegistryValue( + RegistryValue protocolRegValue = RegistryValue.string( 'URL Protocol', - RegistryValueType.string, '', ); String protocolCmdRegKey = 'shell\\open\\command'; - RegistryValue protocolCmdRegValue = RegistryValue( + RegistryValue protocolCmdRegValue = RegistryValue.string( '', - RegistryValueType.string, '"${Platform.resolvedExecutable}" "%1"', ); final regKey = Registry.currentUser.createKey(protocolRegKey); @@ -31,4 +29,4 @@ class Protocol { } } -final protocol = Protocol(); \ No newline at end of file +final protocol = Protocol(); diff --git a/lib/common/render.dart b/lib/common/render.dart index 777ebbc..eb52f96 100644 --- a/lib/common/render.dart +++ b/lib/common/render.dart @@ -22,7 +22,7 @@ class Render { } pause() { - debouncer.call( + throttler.call( DebounceTag.renderPause, _pause, duration: Duration(seconds: 5), @@ -30,11 +30,11 @@ class Render { } resume() { - debouncer.cancel(DebounceTag.renderPause); + throttler.cancel(DebounceTag.renderPause); _resume(); } - void _pause() { + void _pause() async { if (_isPaused) return; _isPaused = true; _beginFrame = _dispatcher.onBeginFrame; diff --git a/lib/common/request.dart b/lib/common/request.dart index bc061da..eee6e5a 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -83,13 +83,19 @@ class Request { Future checkIp({CancelToken? cancelToken}) async { for (final source in _ipInfoSources.entries) { try { - final response = await _dio.get>( - source.key, - cancelToken: cancelToken, - options: Options( - responseType: ResponseType.json, - ), - ); + final response = await Dio() + .get>( + source.key, + cancelToken: cancelToken, + options: Options( + responseType: ResponseType.json, + ), + ) + .timeout( + Duration( + seconds: 30, + ), + ); if (response.statusCode != 200 || response.data == null) { continue; } diff --git a/lib/common/scroll.dart b/lib/common/scroll.dart index 13e2521..828bdcc 100644 --- a/lib/common/scroll.dart +++ b/lib/common/scroll.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'dart:ui'; import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/widgets/scroll.dart'; import 'package:flutter/material.dart'; class BaseScrollBehavior extends MaterialScrollBehavior { @@ -16,8 +17,6 @@ class BaseScrollBehavior extends MaterialScrollBehavior { }; } -class BaseScrollBehavior2 extends ScrollBehavior {} - class HiddenBarScrollBehavior extends BaseScrollBehavior { @override Widget buildScrollbar( @@ -36,8 +35,7 @@ class ShowBarScrollBehavior extends BaseScrollBehavior { Widget child, ScrollableDetails details, ) { - return Scrollbar( - interactive: true, + return CommonAutoHiddenScrollBar( controller: details.controller, child: child, ); diff --git a/lib/common/text.dart b/lib/common/text.dart index 2488cb2..043df40 100644 --- a/lib/common/text.dart +++ b/lib/common/text.dart @@ -1,15 +1,20 @@ +import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; import 'color.dart'; extension TextStyleExtension on TextStyle { - TextStyle get toLight => copyWith(color: color?.toLight); + TextStyle get toLight => copyWith(color: color?.opacity80); - TextStyle get toLighter => copyWith(color: color?.toLighter); + TextStyle get toLighter => copyWith(color: color?.opacity60); TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500); TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); + TextStyle get toJetBrainsMono => copyWith( + fontFamily: FontFamily.jetBrainsMono.value, + ); + TextStyle adjustSize(int size) => copyWith( fontSize: fontSize! + size, ); diff --git a/lib/common/theme.dart b/lib/common/theme.dart new file mode 100644 index 0000000..64fdbd1 --- /dev/null +++ b/lib/common/theme.dart @@ -0,0 +1,39 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; + +class CommonTheme { + final BuildContext context; + final Map _colorMap; + + CommonTheme.of(this.context) : _colorMap = {}; + + Color get darkenSecondaryContainer { + return _colorMap.getCacheValue( + "darkenSecondaryContainer", + context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1), + ); + } + + Color get darkenSecondaryContainerLighter { + return _colorMap.getCacheValue( + "darkenSecondaryContainerLighter", + context.colorScheme.secondaryContainer + .blendDarken(context, factor: 0.1) + .opacity60, + ); + } + + Color get darken2SecondaryContainer { + return _colorMap.getCacheValue( + "darken2SecondaryContainer", + context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2), + ); + } + + Color get darken3PrimaryContainer { + return _colorMap.getCacheValue( + "darken3PrimaryContainer", + context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3), + ); + } +} diff --git a/lib/common/window.dart b/lib/common/window.dart index 384ccc2..2fdbfa1 100755 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -21,12 +21,12 @@ class Window { await windowManager.ensureInitialized(); WindowOptions windowOptions = WindowOptions( size: Size(props.width, props.height), - minimumSize: const Size(380, 500), + minimumSize: const Size(380, 400), ); if (!Platform.isMacOS || version > 10) { await windowManager.setTitleBarStyle(TitleBarStyle.hidden); } - if(!Platform.isMacOS){ + if (!Platform.isMacOS) { final left = props.left ?? 0; final top = props.top ?? 0; final right = left + props.width; @@ -36,7 +36,7 @@ class Window { } else { final displays = await screenRetriever.getAllDisplays(); final isPositionValid = displays.any( - (display) { + (display) { final displayBounds = Rect.fromLTWH( display.visiblePosition!.dx, display.visiblePosition!.dy, @@ -69,8 +69,10 @@ class Window { await windowManager.setSkipTaskbar(false); } - Future isVisible() async { - return await windowManager.isVisible(); + Future get isVisible async { + final value = await windowManager.isVisible(); + commonPrint.log("window visible check: $value"); + return value; } close() async { diff --git a/lib/controller.dart b/lib/controller.dart index 44b154d..65de038 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -10,12 +10,14 @@ import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart'; import 'package:url_launcher/url_launcher.dart'; import 'common/common.dart'; +import 'fragments/profiles/override_profile.dart'; import 'models/models.dart'; class AppController { @@ -242,6 +244,7 @@ class AppController { } Future updateClashConfig([bool? isPatch]) async { + commonPrint.log("update clash patch: ${isPatch ?? false}"); final commonScaffoldState = globalState.homeScaffoldKey.currentState; if (commonScaffoldState?.mounted != true) return; await commonScaffoldState?.loadingRun(() async { @@ -414,6 +417,9 @@ class AppController { Map? data, bool handleError = false, }) async { + if(globalState.isPre){ + return; + } if (data != null) { final tagName = data['tag_name']; final body = data['body']; @@ -520,37 +526,12 @@ class AppController { _ref.read(delayDataSourceProvider.notifier).setDelay(delay); } - toPage( - int index, { - bool hasAnimate = false, - }) { - final navigations = _ref.read(currentNavigationsStateProvider).value; - if (index > navigations.length - 1) { - return; - } - _ref.read(currentPageLabelProvider.notifier).value = - navigations[index].label; - final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage; - final isMobile = - _ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile; - if (isAnimateToPage && isMobile || hasAnimate) { - globalState.pageController?.animateToPage( - index, - duration: kTabScrollDuration, - curve: Curves.easeOut, - ); - } else { - globalState.pageController?.jumpToPage(index); - } + toPage(PageLabel pageLabel) { + _ref.read(currentPageLabelProvider.notifier).value = pageLabel; } toProfiles() { - final index = _ref.read(currentNavigationsStateProvider).value.indexWhere( - (element) => element.label == PageLabel.profiles, - ); - if (index != -1) { - toPage(index); - } + toPage(PageLabel.profiles); } initLink() { @@ -587,17 +568,8 @@ class AppController { Future showDisclaimer() async { return await globalState.showCommonDialog( dismissible: false, - child: AlertDialog( - title: Text(appLocalizations.disclaimer), - content: Container( - width: dialogCommonWidth, - constraints: const BoxConstraints(maxHeight: 200), - child: SingleChildScrollView( - child: SelectableText( - appLocalizations.disclaimerDesc, - ), - ), - ), + child: CommonDialog( + title: appLocalizations.disclaimer, actions: [ TextButton( onPressed: () { @@ -615,6 +587,9 @@ class AppController { child: Text(appLocalizations.agree), ) ], + child: SelectableText( + appLocalizations.disclaimerDesc, + ), ), ) ?? false; @@ -680,9 +655,9 @@ class AppController { addProfileFormURL(url); } - updateViewWidth(double width) { + updateViewSize(Size size) { WidgetsBinding.instance.addPostFrameCallback((_) { - _ref.read(viewWidthProvider.notifier).value = width; + _ref.read(viewSizeProvider.notifier).value = size; }); } @@ -741,10 +716,18 @@ class AppController { final providersPath = await appPath.getProvidersPath(profileId); return await Isolate.run(() async { if (profilePath != null) { - await File(profilePath).delete(recursive: true); + final profileFile = File(profilePath); + final isExists = await profileFile.exists(); + if (isExists) { + profileFile.delete(recursive: true); + } } if (providersPath != null) { - await File(providersPath).delete(recursive: true); + final providersFileDir = File(providersPath); + final isExists = await providersFileDir.exists(); + if (isExists) { + providersFileDir.delete(recursive: true); + } } }); } @@ -798,10 +781,10 @@ class AppController { _ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith(mode: mode), ); - // if (mode == Mode.global) { - // updateCurrentGroupName(GroupName.GLOBAL.name); - // } - // addCheckIpNumDebounce(); + if (mode == Mode.global) { + updateCurrentGroupName(GroupName.GLOBAL.name); + } + addCheckIpNumDebounce(); } updateAutoLaunch() { @@ -813,7 +796,7 @@ class AppController { } updateVisible() async { - final visible = await window?.isVisible(); + final visible = await window?.isVisible; if (visible != null && !visible) { window?.show(); } else { @@ -836,6 +819,38 @@ class AppController { ); } + handleAddOrUpdate(WidgetRef ref, [Rule? rule]) async { + final res = await globalState.showCommonDialog( + child: AddRuleDialog( + rule: rule, + snippet: ref.read( + profileOverrideStateProvider.select( + (state) => state.snippet!, + ), + ), + ), + ); + if (res == null) { + return; + } + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) { + final model = state.copyWith.overrideData!( + rule: state.overrideData!.rule.updateRules( + (rules) { + final index = rules.indexWhere((item) => item.id == res.id); + if (index == -1) { + return List.from([res, ...rules]); + } + return List.from(rules)..[index] = res; + }, + ), + ); + return model; + }, + ); + } + Future exportLogs() async { final logsRaw = _ref.read(logsProvider).list.map( (item) => item.toString(), diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index b9214db..fe7b2f3 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -220,11 +220,12 @@ enum ProxiesIconStyle { enum FontFamily { twEmoji("Twemoji"), + jetBrainsMono("JetBrainsMono"), icon("Icons"); - final String? value; + final String value; - const FontFamily([this.value]); + const FontFamily(this.value); } enum RouteMode { @@ -384,3 +385,65 @@ enum PageLabel { resources, connections, } + +enum RuleAction { + DOMAIN("DOMAIN"), + DOMAIN_SUFFIX("DOMAIN-SUFFIX"), + DOMAIN_KEYWORD("DOMAIN-KEYWORD"), + DOMAIN_REGEX("DOMAIN-REGEX"), + GEOSITE("GEOSITE"), + IP_CIDR("IP-CIDR"), + IP_CIDR6("IP-CIDR6"), + IP_SUFFIX("IP-SUFFIX"), + IP_ASN("IP-ASN"), + GEOIP("GEOIP"), + SRC_GEOIP("SRC-GEOIP"), + SRC_IP_ASN("SRC-IP-ASN"), + SRC_IP_CIDR("SRC-IP-CIDR"), + SRC_IP_SUFFIX("SRC-IP-SUFFIX"), + DST_PORT("DST-PORT"), + SRC_PORT("SRC-PORT"), + IN_PORT("IN-PORT"), + IN_TYPE("IN-TYPE"), + IN_USER("IN-USER"), + IN_NAME("IN-NAME"), + PROCESS_PATH("PROCESS-PATH"), + PROCESS_PATH_REGEX("PROCESS-PATH-REGEX"), + PROCESS_NAME("PROCESS-NAME"), + PROCESS_NAME_REGEX("PROCESS-NAME-REGEX"), + UID("UID"), + NETWORK("NETWORK"), + DSCP("DSCP"), + RULE_SET("RULE-SET"), + AND("AND"), + OR("OR"), + NOT("NOT"), + SUB_RULE("SUB-RULE"), + MATCH("MATCH"); + + final String value; + + const RuleAction(this.value); +} + +extension RuleActionExt on RuleAction { + bool get hasParams => [ + RuleAction.GEOIP, + RuleAction.IP_ASN, + RuleAction.SRC_IP_ASN, + RuleAction.IP_CIDR, + RuleAction.IP_CIDR6, + RuleAction.IP_SUFFIX, + RuleAction.RULE_SET, + ].contains(this); +} + +enum OverrideRuleType { + override, + added, +} + +enum RuleTarget { + DIRECT, + REJECT, +} diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 30bd8f8..b62bd28 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -150,9 +150,17 @@ class _AccessFragmentState extends ConsumerState { return IconButton( onPressed: () async { final res = await showSheet( - title: appLocalizations.proxiesSetting, context: context, - body: AccessControlPanel(), + props: SheetProps( + isScrollControlled: true, + ), + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: AccessControlPanel(), + title: appLocalizations.proxiesSetting, + ); + }, ); if (res == 1) { _intelligentSelected(); @@ -763,17 +771,19 @@ class _AccessControlPanelState extends ConsumerState { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 32), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ..._buildModeSetting(), - ..._buildSortSetting(), - ..._buildSourceSetting(), - ..._buildActionSetting(), - ], + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildModeSetting(), + ..._buildSortSetting(), + ..._buildSourceSetting(), + ..._buildActionSetting(), + ], + ), ), ); } diff --git a/lib/fragments/application_setting.dart b/lib/fragments/application_setting.dart index 15fbc72..baea802 100644 --- a/lib/fragments/application_setting.dart +++ b/lib/fragments/application_setting.dart @@ -276,8 +276,8 @@ class ApplicationSettingFragment extends StatelessWidget { AutoRunItem(), if (Platform.isAndroid) ...[ HiddenItem(), - AnimateTabItem(), ], + AnimateTabItem(), OpenLogsItem(), CloseConnectionsItem(), UsageItem(), diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index c16e10c..b730490 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -6,6 +6,7 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; 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/list.dart'; import 'package:fl_clash/widgets/text.dart'; @@ -174,7 +175,7 @@ class BackupAndRecovery extends ConsumerWidget { future: client!.pingCompleter.future, builder: (_, snapshot) { return Center( - child: FadeBox( + child: FadeThroughBox( child: snapshot.connectionState == ConnectionState.waiting ? const SizedBox( @@ -275,30 +276,27 @@ class _RecoveryOptionsDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.recovery), - contentPadding: const EdgeInsets.symmetric( + return CommonDialog( + title: appLocalizations.recovery, + padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 16, ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - ListItem( - onTap: () { - _handleOnTab(RecoveryOption.onlyProfiles); - }, - title: Text(appLocalizations.recoveryProfiles), - ), - ListItem( - onTap: () { - _handleOnTab(RecoveryOption.all); - }, - title: Text(appLocalizations.recoveryAll), - ) - ], - ), + child: Wrap( + children: [ + ListItem( + onTap: () { + _handleOnTab(RecoveryOption.onlyProfiles); + }, + title: Text(appLocalizations.recoveryProfiles), + ), + ListItem( + onTap: () { + _handleOnTab(RecoveryOption.all); + }, + title: Text(appLocalizations.recoveryAll), + ) + ], ), ); } @@ -351,78 +349,8 @@ class _WebDAVFormDialogState extends ConsumerState { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.webDAVConfiguration), - content: Form( - key: _formKey, - child: SizedBox( - width: dialogCommonWidth, - child: Wrap( - runSpacing: 16, - children: [ - TextFormField( - controller: uriController, - maxLines: 5, - minLines: 1, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link), - border: const OutlineInputBorder(), - labelText: appLocalizations.address, - helperText: appLocalizations.addressHelp, - ), - validator: (String? value) { - if (value == null || value.isEmpty || !value.isUrl) { - return appLocalizations.addressTip; - } - return null; - }, - ), - TextFormField( - controller: userController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.account_circle), - border: const OutlineInputBorder(), - labelText: appLocalizations.account, - ), - validator: (String? value) { - if (value == null || value.isEmpty) { - return appLocalizations.accountTip; - } - return null; - }, - ), - ValueListenableBuilder( - valueListenable: _obscureController, - builder: (_, obscure, __) { - return TextFormField( - controller: passwordController, - obscureText: obscure, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.password), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: Icon( - obscure ? Icons.visibility : Icons.visibility_off, - ), - onPressed: () { - _obscureController.value = !obscure; - }, - ), - labelText: appLocalizations.password, - ), - validator: (String? value) { - if (value == null || value.isEmpty) { - return appLocalizations.passwordTip; - } - return null; - }, - ); - }, - ), - ], - ), - ), - ), + return CommonDialog( + title: appLocalizations.webDAVConfiguration, actions: [ if (widget.dav != null) TextButton( @@ -434,6 +362,73 @@ class _WebDAVFormDialogState extends ConsumerState { child: Text(appLocalizations.save), ) ], + child: Form( + key: _formKey, + child: Wrap( + runSpacing: 16, + children: [ + TextFormField( + controller: uriController, + maxLines: 5, + minLines: 1, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link), + border: const OutlineInputBorder(), + labelText: appLocalizations.address, + helperText: appLocalizations.addressHelp, + ), + validator: (String? value) { + if (value == null || value.isEmpty || !value.isUrl) { + return appLocalizations.addressTip; + } + return null; + }, + ), + TextFormField( + controller: userController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.account_circle), + border: const OutlineInputBorder(), + labelText: appLocalizations.account, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.accountTip; + } + return null; + }, + ), + ValueListenableBuilder( + valueListenable: _obscureController, + builder: (_, obscure, __) { + return TextFormField( + controller: passwordController, + obscureText: obscure, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.password), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: Icon( + obscure ? Icons.visibility : Icons.visibility_off, + ), + onPressed: () { + _obscureController.value = !obscure; + }, + ), + labelText: appLocalizations.password, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.passwordTip; + } + return null; + }, + ); + }, + ), + ], + ), + ), ); } } diff --git a/lib/fragments/config/config.dart b/lib/fragments/config/config.dart index 10659d2..5cbb8bf 100644 --- a/lib/fragments/config/config.dart +++ b/lib/fragments/config/config.dart @@ -2,8 +2,13 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/fragments/config/dns.dart'; import 'package:fl_clash/fragments/config/general.dart'; import 'package:fl_clash/fragments/config/network.dart'; +import 'package:fl_clash/models/clash_config.dart'; +import 'package:fl_clash/providers/config.dart' show patchClashConfigProvider; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../state.dart'; class ConfigFragment extends StatefulWidget { const ConfigFragment({super.key}); @@ -16,18 +21,6 @@ class _ConfigFragmentState extends State { @override Widget build(BuildContext context) { List items = [ - ListItem.open( - title: Text(appLocalizations.network), - subtitle: Text(appLocalizations.networkDesc), - leading: const Icon(Icons.vpn_key), - delegate: OpenDelegate( - title: appLocalizations.network, - isScaffold: true, - isBlur: false, - extendPageWidth: 360, - widget: const NetworkListView(), - ), - ), ListItem.open( title: Text(appLocalizations.general), subtitle: Text(appLocalizations.generalDesc), @@ -37,20 +30,52 @@ class _ConfigFragmentState extends State { widget: generateListView( generalItems, ), - isBlur: false, - extendPageWidth: 360, + blur: false, + ), + ), + ListItem.open( + title: Text(appLocalizations.network), + subtitle: Text(appLocalizations.networkDesc), + leading: const Icon(Icons.vpn_key), + delegate: OpenDelegate( + title: appLocalizations.network, + blur: false, + widget: const NetworkListView(), ), ), ListItem.open( title: const Text("DNS"), subtitle: Text(appLocalizations.dnsDesc), leading: const Icon(Icons.dns), - delegate: const OpenDelegate( + delegate: OpenDelegate( title: "DNS", - widget: DnsListView(), - isScaffold: true, - isBlur: false, - extendPageWidth: 360, + action: Consumer(builder: (_, ref, __) { + return IconButton( + onPressed: () async { + final res = await globalState.showMessage( + title: appLocalizations.reset, + message: TextSpan( + text: appLocalizations.resetTip, + ), + ); + if (res != true) { + return; + } + + ref.read(patchClashConfigProvider.notifier).updateState( + (state) => state.copyWith( + dns: defaultDns, + ), + ); + }, + tooltip: appLocalizations.reset, + icon: const Icon( + Icons.replay, + ), + ); + }), + widget: const DnsListView(), + blur: false, ), ) ]; diff --git a/lib/fragments/config/dns.dart b/lib/fragments/config/dns.dart index e162b8d..b851447 100644 --- a/lib/fragments/config/dns.dart +++ b/lib/fragments/config/dns.dart @@ -1,8 +1,6 @@ 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/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'; @@ -205,7 +203,7 @@ class FakeIpFilterItem extends StatelessWidget { return ListItem.open( title: Text(appLocalizations.fakeipFilter), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.fakeipFilter, widget: Consumer( builder: (_, ref, __) { @@ -213,7 +211,7 @@ class FakeIpFilterItem extends StatelessWidget { patchClashConfigProvider .select((state) => state.dns.fakeIpFilter), ); - return ListPage( + return ListInputPage( title: appLocalizations.fakeipFilter, items: fakeIpFilter, titleBuilder: (item) => Text(item), @@ -227,7 +225,6 @@ class FakeIpFilterItem extends StatelessWidget { ); }, ), - extendPageWidth: 360, ), ); } @@ -242,14 +239,14 @@ class DefaultNameserverItem extends StatelessWidget { title: Text(appLocalizations.defaultNameserver), subtitle: Text(appLocalizations.defaultNameserverDesc), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.defaultNameserver, widget: Consumer(builder: (_, ref, __) { final defaultNameserver = ref.watch( patchClashConfigProvider .select((state) => state.dns.defaultNameserver), ); - return ListPage( + return ListInputPage( title: appLocalizations.defaultNameserver, items: defaultNameserver, titleBuilder: (item) => Text(item), @@ -262,7 +259,6 @@ class DefaultNameserverItem extends StatelessWidget { }, ); }), - extendPageWidth: 360, ), ); } @@ -278,13 +274,13 @@ class NameserverItem extends StatelessWidget { subtitle: Text(appLocalizations.nameserverDesc), delegate: OpenDelegate( title: appLocalizations.nameserver, - isBlur: false, + blur: false, widget: Consumer(builder: (_, ref, __) { final nameserver = ref.watch( patchClashConfigProvider.select((state) => state.dns.nameserver), ); - return ListPage( - title: "域名服务器", + return ListInputPage( + title: appLocalizations.nameserver, items: nameserver, titleBuilder: (item) => Text(item), onChange: (items) { @@ -296,7 +292,6 @@ class NameserverItem extends StatelessWidget { }, ); }), - extendPageWidth: 360, ), ); } @@ -357,28 +352,27 @@ class NameserverPolicyItem extends StatelessWidget { title: Text(appLocalizations.nameserverPolicy), subtitle: Text(appLocalizations.nameserverPolicyDesc), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.nameserverPolicy, widget: Consumer(builder: (_, ref, __) { final nameserverPolicy = ref.watch( patchClashConfigProvider .select((state) => state.dns.nameserverPolicy), ); - return ListPage( + return MapInputPage( title: appLocalizations.nameserverPolicy, - items: nameserverPolicy.entries, + map: nameserverPolicy, titleBuilder: (item) => Text(item.key), subtitleBuilder: (item) => Text(item.value), - onChange: (items) { + onChange: (value) { ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith.dns( - nameserverPolicy: Map.fromEntries(items), + nameserverPolicy: value, ), ); }, ); }), - extendPageWidth: 360, ), ); } @@ -393,7 +387,7 @@ class ProxyServerNameserverItem extends StatelessWidget { title: Text(appLocalizations.proxyNameserver), subtitle: Text(appLocalizations.proxyNameserverDesc), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.proxyNameserver, widget: Consumer( builder: (_, ref, __) { @@ -401,7 +395,7 @@ class ProxyServerNameserverItem extends StatelessWidget { patchClashConfigProvider .select((state) => state.dns.proxyServerNameserver), ); - return ListPage( + return ListInputPage( title: appLocalizations.proxyNameserver, items: proxyServerNameserver, titleBuilder: (item) => Text(item), @@ -415,7 +409,6 @@ class ProxyServerNameserverItem extends StatelessWidget { ); }, ), - extendPageWidth: 360, ), ); } @@ -430,13 +423,13 @@ class FallbackItem extends StatelessWidget { title: Text(appLocalizations.fallback), subtitle: Text(appLocalizations.fallbackDesc), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.fallback, widget: Consumer(builder: (_, ref, __) { final fallback = ref.watch( patchClashConfigProvider.select((state) => state.dns.fallback), ); - return ListPage( + return ListInputPage( title: appLocalizations.fallback, items: fallback, titleBuilder: (item) => Text(item), @@ -449,7 +442,6 @@ class FallbackItem extends StatelessWidget { }, ); }), - extendPageWidth: 360, ), ); } @@ -518,14 +510,14 @@ class GeositeItem extends StatelessWidget { return ListItem.open( title: const Text("Geosite"), delegate: OpenDelegate( - isBlur: false, + blur: false, title: "Geosite", widget: Consumer(builder: (_, ref, __) { final geosite = ref.watch( patchClashConfigProvider .select((state) => state.dns.fallbackFilter.geosite), ); - return ListPage( + return ListInputPage( title: "Geosite", items: geosite, titleBuilder: (item) => Text(item), @@ -538,7 +530,6 @@ class GeositeItem extends StatelessWidget { }, ); }), - extendPageWidth: 360, ), ); } @@ -552,14 +543,14 @@ class IpcidrItem extends StatelessWidget { return ListItem.open( title: Text(appLocalizations.ipcidr), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.ipcidr, widget: Consumer(builder: (_, ref, ___) { final ipcidr = ref.watch( patchClashConfigProvider .select((state) => state.dns.fallbackFilter.ipcidr), ); - return ListPage( + return ListInputPage( title: appLocalizations.ipcidr, items: ipcidr, titleBuilder: (item) => Text(item), @@ -572,7 +563,6 @@ class IpcidrItem extends StatelessWidget { }, ); }), - extendPageWidth: 360, ), ); } @@ -586,14 +576,14 @@ class DomainItem extends StatelessWidget { return ListItem.open( title: Text(appLocalizations.domain), delegate: OpenDelegate( - isBlur: false, + blur: false, title: appLocalizations.domain, widget: Consumer(builder: (_, ref, __) { final domain = ref.watch( patchClashConfigProvider .select((state) => state.dns.fallbackFilter.domain), ); - return ListPage( + return ListInputPage( title: appLocalizations.domain, items: domain, titleBuilder: (item) => Text(item), @@ -606,7 +596,6 @@ class DomainItem extends StatelessWidget { }, ); }), - extendPageWidth: 360, ), ); } @@ -671,39 +660,8 @@ const dnsItems = [ class DnsListView extends ConsumerWidget { const DnsListView({super.key}); - _initActions(BuildContext context, WidgetRef ref) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - context.commonScaffoldState?.actions = [ - IconButton( - onPressed: () async { - final res = await globalState.showMessage( - title: appLocalizations.reset, - message: TextSpan( - text: appLocalizations.resetTip, - ), - ); - if (res != true) { - return; - } - - ref.read(patchClashConfigProvider.notifier).updateState( - (state) => state.copyWith( - dns: defaultDns, - ), - ); - }, - tooltip: appLocalizations.reset, - icon: const Icon( - Icons.replay, - ), - ) - ]; - }); - } - @override Widget build(BuildContext context, ref) { - _initActions(context, ref); return generateListView( dnsItems, ); diff --git a/lib/fragments/config/general.dart b/lib/fragments/config/general.dart index 1dabbf7..71c96c4 100644 --- a/lib/fragments/config/general.dart +++ b/lib/fragments/config/general.dart @@ -199,28 +199,27 @@ class HostsItem extends StatelessWidget { title: const Text("Hosts"), subtitle: Text(appLocalizations.hostsDesc), delegate: OpenDelegate( - isBlur: false, + blur: false, title: "Hosts", widget: Consumer( builder: (_, ref, __) { final hosts = ref .watch(patchClashConfigProvider.select((state) => state.hosts)); - return ListPage( + return MapInputPage( title: "Hosts", - items: hosts.entries, + map: hosts, titleBuilder: (item) => Text(item.key), subtitleBuilder: (item) => Text(item.value), - onChange: (items) { + onChange: (value) { ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith( - hosts: Map.fromEntries(items), + hosts: value, ), ); }, ); }, ), - extendPageWidth: 360, ), ); } diff --git a/lib/fragments/config/network.dart b/lib/fragments/config/network.dart index c94b485..1826ad9 100644 --- a/lib/fragments/config/network.dart +++ b/lib/fragments/config/network.dart @@ -224,15 +224,14 @@ class BypassDomainItem extends StatelessWidget { title: Text(appLocalizations.bypassDomain), subtitle: Text(appLocalizations.bypassDomainDesc), delegate: OpenDelegate( - isBlur: false, - isScaffold: true, + blur: false, title: appLocalizations.bypassDomain, widget: Consumer( builder: (_, ref, __) { _initActions(context, ref); final bypassDomain = ref.watch( networkSettingProvider.select((state) => state.bypassDomain)); - return ListPage( + return ListInputPage( title: appLocalizations.bypassDomain, items: bypassDomain, titleBuilder: (item) => Text(item), @@ -246,7 +245,6 @@ class BypassDomainItem extends StatelessWidget { ); }, ), - extendPageWidth: 360, ), ); } @@ -298,14 +296,14 @@ class RouteAddressItem extends ConsumerWidget { title: Text(appLocalizations.routeAddress), subtitle: Text(appLocalizations.routeAddressDesc), delegate: OpenDelegate( - isBlur: false, - isScaffold: true, + blur: false, + maxWidth: 360, title: appLocalizations.routeAddress, widget: Consumer( builder: (_, ref, __) { final routeAddress = ref.watch(patchClashConfigProvider .select((state) => state.tun.routeAddress)); - return ListPage( + return ListInputPage( title: appLocalizations.routeAddress, items: routeAddress, titleBuilder: (item) => Text(item), @@ -319,7 +317,6 @@ class RouteAddressItem extends ConsumerWidget { ); }, ), - extendPageWidth: 360, ), ); } diff --git a/lib/fragments/connection/connections.dart b/lib/fragments/connection/connections.dart index 8ae9e23..99af1b7 100644 --- a/lib/fragments/connection/connections.dart +++ b/lib/fragments/connection/connections.dart @@ -125,7 +125,7 @@ class _ConnectionsFragmentState extends ConsumerState return ConnectionItem( key: Key(connection.id), connection: connection, - onClick: (value) { + onClickKeyword: (value) { context.commonScaffoldState?.addKeyword(value); }, trailing: IconButton( diff --git a/lib/fragments/connection/item.dart b/lib/fragments/connection/item.dart index eef2d88..9ce9113 100644 --- a/lib/fragments/connection/item.dart +++ b/lib/fragments/connection/item.dart @@ -9,40 +9,15 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -class FindProcessBuilder extends StatelessWidget { - final Widget Function(bool value) builder; - - const FindProcessBuilder({ - super.key, - required this.builder, - }); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (_, ref, __) { - final value = ref.watch( - patchClashConfigProvider.select( - (state) => - state.findProcessMode == FindProcessMode.always && - Platform.isAndroid, - ), - ); - return builder(value); - }, - ); - } -} - -class ConnectionItem extends StatelessWidget { +class ConnectionItem extends ConsumerWidget { final Connection connection; - final Function(String)? onClick; + final Function(String)? onClickKeyword; final Widget? trailing; const ConnectionItem({ super.key, required this.connection, - this.onClick, + this.onClickKeyword, this.trailing, }); @@ -59,7 +34,14 @@ class ConnectionItem extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { + final value = ref.watch( + patchClashConfigProvider.select( + (state) => + state.findProcessMode == FindProcessMode.always && + Platform.isAndroid, + ), + ); final title = Text( connection.desc, style: context.textTheme.bodyLarge, @@ -86,70 +68,143 @@ class ConnectionItem extends StatelessWidget { CommonChip( label: chain, onPressed: () { - if (onClick == null) return; - onClick!(chain); + if (onClickKeyword == null) return; + onClickKeyword!(chain); }, ), ], ), ], ); - if (!Platform.isAndroid) { - return ListItem( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - tileTitleAlignment: ListTileTitleAlignment.titleHeight, - title: title, - subtitle: subTitle, - trailing: trailing, - ); - } - return FindProcessBuilder( - builder: (bool value) { - final leading = value - ? GestureDetector( - onTap: () { - if (onClick == null) return; - final process = connection.metadata.process; - if (process.isEmpty) return; - onClick!(process); - }, - child: Container( - margin: const EdgeInsets.only(top: 4), - width: 48, - height: 48, - child: FutureBuilder( - future: _getPackageIcon(connection), - builder: (_, snapshot) { - if (!snapshot.hasData && snapshot.data == null) { - return Container(); - } else { - return Image( - image: snapshot.data!, - gaplessPlayback: true, - width: 48, - height: 48, - ); - } - }, - ), - ), - ) - : null; + return CommonPopupBox( + targetBuilder: (open) { return ListItem( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 4, ), tileTitleAlignment: ListTileTitleAlignment.titleHeight, - leading: leading, + leading: value + ? GestureDetector( + onTap: () { + if (onClickKeyword == null) return; + final process = connection.metadata.process; + if (process.isEmpty) return; + onClickKeyword!(process); + }, + child: Container( + margin: const EdgeInsets.only(top: 4), + width: 48, + height: 48, + child: FutureBuilder( + future: _getPackageIcon(connection), + builder: (_, snapshot) { + if (!snapshot.hasData && snapshot.data == null) { + return Container(); + } else { + return Image( + image: snapshot.data!, + gaplessPlayback: true, + width: 48, + height: 48, + ); + } + }, + ), + ), + ) + : null, title: title, subtitle: subTitle, trailing: trailing, ); + // return InkWell( + // child: GestureDetector( + // onLongPressStart: (details) { + // if (!system.isDesktop) { + // return; + // } + // open( + // offset: details.localPosition.translate( + // 0, + // -12, + // ), + // ); + // }, + // onSecondaryTapDown: (details) { + // if (!system.isDesktop) { + // return; + // } + // open( + // offset: details.localPosition.translate( + // 0, + // -12, + // ), + // ); + // }, + // child: ListItem( + // padding: const EdgeInsets.symmetric( + // horizontal: 16, + // vertical: 4, + // ), + // tileTitleAlignment: ListTileTitleAlignment.titleHeight, + // leading: value + // ? GestureDetector( + // onTap: () { + // if (onClickKeyword == null) return; + // final process = connection.metadata.process; + // if (process.isEmpty) return; + // onClickKeyword!(process); + // }, + // child: Container( + // margin: const EdgeInsets.only(top: 4), + // width: 48, + // height: 48, + // child: FutureBuilder( + // future: _getPackageIcon(connection), + // builder: (_, snapshot) { + // if (!snapshot.hasData && snapshot.data == null) { + // return Container(); + // } else { + // return Image( + // image: snapshot.data!, + // gaplessPlayback: true, + // width: 48, + // height: 48, + // ); + // } + // }, + // ), + // ), + // ) + // : null, + // title: title, + // subtitle: subTitle, + // trailing: trailing, + // ), + // ), + // onTap: () {}, + // ); }, + popup: CommonPopupMenu( + minWidth: 160, + items: [ + PopupMenuItemData( + label: "编辑规则", + onPressed: () { + // _handleShowEditExtendPage(context); + }, + ), + PopupMenuItemData( + label: "设置直连", + onPressed: () {}, + ), + PopupMenuItemData( + label: "一键屏蔽", + onPressed: () {}, + ), + ], + ), ); } } diff --git a/lib/fragments/connection/requests.dart b/lib/fragments/connection/requests.dart index 2971472..69e04e4 100644 --- a/lib/fragments/connection/requests.dart +++ b/lib/fragments/connection/requests.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; @@ -20,6 +22,7 @@ class RequestsFragment extends ConsumerStatefulWidget { class _RequestsFragmentState extends ConsumerState with PageMixin { + final GlobalKey _key = GlobalKey(); final _requestsStateNotifier = ValueNotifier(const ConnectionsState()); List _requests = []; @@ -28,8 +31,6 @@ class _RequestsFragmentState extends ConsumerState initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, ); - final FixedMap _cacheDynamicHeightMap = FixedMap(1000); - double _currentMaxWidth = 0; @override @@ -78,10 +79,6 @@ class _RequestsFragmentState extends ConsumerState } double _calcCacheHeight(Connection item) { - final cacheHeight = _cacheDynamicHeightMap.get(item.id); - if (cacheHeight != null) { - return cacheHeight; - } final size = globalState.measure.computeTextSize( Text( item.desc, @@ -102,14 +99,13 @@ class _RequestsFragmentState extends ConsumerState final lines = (chainSize.height / baseHeight).round(); final computerHeight = size.height + chainSize.height + 24 + 24 * (lines - 1); - _cacheDynamicHeightMap.put(item.id, computerHeight); return computerHeight; } _handleTryClearCache(double maxWidth) { if (_currentMaxWidth != maxWidth) { _currentMaxWidth = maxWidth; - _cacheDynamicHeightMap.clear(); + _key.currentState?.clearCache(); } } @@ -118,7 +114,6 @@ class _RequestsFragmentState extends ConsumerState _requestsStateNotifier.dispose(); _scrollController.dispose(); _currentMaxWidth = 0; - _cacheDynamicHeightMap.clear(); super.dispose(); } @@ -143,9 +138,19 @@ class _RequestsFragmentState extends ConsumerState Widget build(BuildContext context) { return LayoutBuilder( builder: (_, constraints) { - return FindProcessBuilder(builder: (value) { - _handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); - return ValueListenableBuilder( + return Consumer( + builder: (_, ref, child) { + final value = ref.watch( + patchClashConfigProvider.select( + (state) => + state.findProcessMode == FindProcessMode.always && + Platform.isAndroid, + ), + ); + _handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); + return child!; + }, + child: ValueListenableBuilder( valueListenable: _requestsStateNotifier, builder: (_, state, __) { final connections = state.list; @@ -159,7 +164,7 @@ class _RequestsFragmentState extends ConsumerState (connection) => ConnectionItem( key: Key(connection.id), connection: connection, - onClick: (value) { + onClickKeyword: (value) { context.commonScaffoldState?.addKeyword(value); }, ), @@ -179,12 +184,13 @@ class _RequestsFragmentState extends ConsumerState }, child: CommonScrollBar( controller: _scrollController, - child: ListView.builder( + child: CacheItemExtentListView( + key: _key, reverse: true, shrinkWrap: true, physics: NextClampingScrollPhysics(), controller: _scrollController, - itemExtentBuilder: (index, __) { + itemExtentBuilder: (index) { final widget = items[index]; if (widget.runtimeType == Divider) { return 0; @@ -199,13 +205,21 @@ class _RequestsFragmentState extends ConsumerState 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; + }, ), ), ), ); }, - ); - }); + ), + ); }, ); } diff --git a/lib/fragments/dashboard/widgets/intranet_ip.dart b/lib/fragments/dashboard/widgets/intranet_ip.dart index 7022a36..8af89d6 100644 --- a/lib/fragments/dashboard/widgets/intranet_ip.dart +++ b/lib/fragments/dashboard/widgets/intranet_ip.dart @@ -31,7 +31,7 @@ class IntranetIP extends StatelessWidget { child: Consumer( builder: (_, ref, __) { final localIp = ref.watch(localIpProvider); - return FadeBox( + return FadeThroughBox( child: localIp != null ? TooltipText( text: Text( diff --git a/lib/fragments/dashboard/widgets/memory_info.dart b/lib/fragments/dashboard/widgets/memory_info.dart index ed239cd..3e0a90a 100644 --- a/lib/fragments/dashboard/widgets/memory_info.dart +++ b/lib/fragments/dashboard/widgets/memory_info.dart @@ -39,7 +39,7 @@ class _MemoryInfoState extends State { _memoryInfoStateNotifier.value = TrafficValue( value: clashLib != null ? rss : await clashCore.getMemory() + rss, ); - timer = Timer(Duration(seconds: 5), () async { + timer = Timer(Duration(seconds: 2), () async { _updateMemory(); }); }); @@ -47,13 +47,8 @@ class _MemoryInfoState extends State { @override Widget build(BuildContext context) { - final darkenLighter = context.colorScheme.secondaryContainer - .blendDarken(context, factor: 0.1) - .toLighter; - final darken = context.colorScheme.secondaryContainer - .blendDarken(context, factor: 0.1); return SizedBox( - height: getWidgetHeight(2), + height: getWidgetHeight(1), child: CommonCard( info: Info( iconData: Icons.memory, @@ -76,43 +71,94 @@ class _MemoryInfoState extends State { children: [ Text( trafficValue.showValue, - style: context.textTheme.titleLarge?.toLight, + style: + context.textTheme.bodyMedium?.toLight.adjustSize(1), ), SizedBox( width: 8, ), Text( trafficValue.showUnit, - style: context.textTheme.titleLarge?.toLight, + style: + context.textTheme.bodyMedium?.toLight.adjustSize(1), ) ], ), ); }, ), - Flexible( - child: Stack( - children: [ - Positioned.fill( - child: WaveView( - waveAmplitude: 12.0, - waveFrequency: 0.35, - waveColor: darkenLighter, - ), - ), - Positioned.fill( - child: WaveView( - waveAmplitude: 12.0, - waveFrequency: 0.9, - waveColor: darken, - ), - ), - ], - ), - ), ], ), ), ); } } + +// class AnimatedCounter extends StatefulWidget { +// final double value; +// final TextStyle? style; +// +// const AnimatedCounter({ +// super.key, +// required this.value, +// this.style, +// }); +// +// @override +// State createState() => _AnimatedCounterState(); +// } +// +// class _AnimatedCounterState extends State { +// late double _previousValue; +// late double _currentValue; +// +// @override +// void initState() { +// super.initState(); +// _previousValue = widget.value; +// _currentValue = widget.value; +// } +// +// @override +// void didUpdateWidget(AnimatedCounter oldWidget) { +// super.didUpdateWidget(oldWidget); +// if (oldWidget.value != widget.value) { +// // if (_previousValue == _currentValue) { +// // _previousValue = widget.value; +// // _currentValue = widget.value; +// // return; +// // } +// _currentValue = widget.value; +// } +// } +// +// @override +// void dispose() { +// super.dispose(); +// } +// +// @override +// Widget build(BuildContext context) { +// return Text( +// _currentValue.fixed(decimals: 1), +// style: widget.style, +// ); +// return TweenAnimationBuilder( +// tween: Tween( +// begin: _previousValue, +// end: _currentValue, +// ), +// onEnd: () { +// _previousValue = _currentValue; +// }, +// duration: Duration(seconds: 6), +// curve: Curves.easeOut, +// builder: (_, value, ___) { +// return Text( +// value.fixed(decimals: 1), +// style: widget.style, +// ); +// }, +// ); +// } +// } diff --git a/lib/fragments/dashboard/widgets/network_detection.dart b/lib/fragments/dashboard/widgets/network_detection.dart index bec2d8a..37fbd52 100644 --- a/lib/fragments/dashboard/widgets/network_detection.dart +++ b/lib/fragments/dashboard/widgets/network_detection.dart @@ -12,7 +12,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; final _networkDetectionState = ValueNotifier( const NetworkDetectionState( - isTesting: true, + isTesting: false, + isLoading: true, ipInfo: null, ), ); @@ -28,7 +29,6 @@ class _NetworkDetectionState extends ConsumerState { bool? _preIsStart; Timer? _setTimeoutTimer; CancelToken? cancelToken; - Completer? checkedCompleter; @override void initState() { @@ -37,11 +37,14 @@ class _NetworkDetectionState extends ConsumerState { _startCheck(); } }); + if (!_networkDetectionState.value.isTesting && + _networkDetectionState.value.isLoading) { + _startCheck(); + } super.initState(); } _startCheck() async { - await checkedCompleter?.future; if (cancelToken != null) { cancelToken!.cancel(); cancelToken = null; @@ -64,7 +67,7 @@ class _NetworkDetectionState extends ConsumerState { } _clearSetTimeoutTimer(); _networkDetectionState.value = _networkDetectionState.value.copyWith( - isTesting: true, + isLoading: true, ipInfo: null, ); _preIsStart = isStart; @@ -74,16 +77,16 @@ class _NetworkDetectionState extends ConsumerState { } cancelToken = CancelToken(); try { + _networkDetectionState.value = _networkDetectionState.value.copyWith( + isTesting: true, + ); final ipInfo = await request.checkIp(cancelToken: cancelToken); + _networkDetectionState.value = _networkDetectionState.value.copyWith( + isTesting: false, + ); if (ipInfo != null) { - checkedCompleter = Completer(); - checkedCompleter?.complete( - Future.delayed( - Duration(milliseconds: 3000), - ), - ); _networkDetectionState.value = _networkDetectionState.value.copyWith( - isTesting: false, + isLoading: false, ipInfo: ipInfo, ); return; @@ -91,14 +94,14 @@ class _NetworkDetectionState extends ConsumerState { _clearSetTimeoutTimer(); _setTimeoutTimer = Timer(const Duration(milliseconds: 300), () { _networkDetectionState.value = _networkDetectionState.value.copyWith( - isTesting: false, + isLoading: false, ipInfo: null, ); }); } catch (e) { if (e.toString() == "cancelled") { _networkDetectionState.value = _networkDetectionState.value.copyWith( - isTesting: true, + isLoading: true, ipInfo: null, ); } @@ -136,7 +139,7 @@ class _NetworkDetectionState extends ConsumerState { valueListenable: _networkDetectionState, builder: (_, state, __) { final ipInfo = state.ipInfo; - final isTesting = state.isTesting; + final isLoading = state.isLoading; return CommonCard( onPressed: () {}, child: Column( @@ -218,7 +221,7 @@ class _NetworkDetectionState extends ConsumerState { ), child: SizedBox( height: globalState.measure.bodyMediumHeight + 2, - child: FadeBox( + child: FadeThroughBox( child: ipInfo != null ? TooltipText( text: Text( @@ -229,8 +232,8 @@ class _NetworkDetectionState extends ConsumerState { overflow: TextOverflow.ellipsis, ), ) - : FadeBox( - child: isTesting == false && ipInfo == null + : FadeThroughBox( + child: isLoading == false && ipInfo == null ? Text( "timeout", style: context.textTheme.bodyMedium diff --git a/lib/fragments/dashboard/widgets/network_speed.dart b/lib/fragments/dashboard/widgets/network_speed.dart index e9e8bd0..df3befd 100644 --- a/lib/fragments/dashboard/widgets/network_speed.dart +++ b/lib/fragments/dashboard/widgets/network_speed.dart @@ -41,7 +41,7 @@ class _NetworkSpeedState extends State { @override Widget build(BuildContext context) { - final color = context.colorScheme.onSurfaceVariant.toLight; + final color = context.colorScheme.onSurfaceVariant.opacity80; return SizedBox( height: getWidgetHeight(2), child: CommonCard( diff --git a/lib/fragments/dashboard/widgets/outbound_mode.dart b/lib/fragments/dashboard/widgets/outbound_mode.dart index c1a52b9..4c177b5 100644 --- a/lib/fragments/dashboard/widgets/outbound_mode.dart +++ b/lib/fragments/dashboard/widgets/outbound_mode.dart @@ -38,7 +38,7 @@ class OutboundMode extends StatelessWidget { for (final item in Mode.values) Flexible( child: ListItem.radio( - prue: true, + dense: true, horizontalTitleGap: 4, padding: const EdgeInsets.only( left: 12, diff --git a/lib/fragments/dashboard/widgets/quick_options.dart b/lib/fragments/dashboard/widgets/quick_options.dart index c330018..22e4de5 100644 --- a/lib/fragments/dashboard/widgets/quick_options.dart +++ b/lib/fragments/dashboard/widgets/quick_options.dart @@ -16,13 +16,20 @@ class TUNButton extends StatelessWidget { onPressed: () { showSheet( context: context, - body: generateListView(generateSection( - items: [ - if (system.isDesktop) const TUNItem(), - const TunStackItem(), - ], - )), - title: appLocalizations.tun, + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: generateListView( + generateSection( + items: [ + if (system.isDesktop) const TUNItem(), + const TunStackItem(), + ], + ), + ), + title: appLocalizations.tun, + ); + }, ); }, info: Info( @@ -89,15 +96,20 @@ class SystemProxyButton extends StatelessWidget { onPressed: () { showSheet( context: context, - body: generateListView( - generateSection( - items: [ - SystemProxyItem(), - BypassDomainItem(), - ], - ), - ), - title: appLocalizations.systemProxy, + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: generateListView( + generateSection( + items: [ + SystemProxyItem(), + BypassDomainItem(), + ], + ), + ), + title: appLocalizations.systemProxy, + ); + }, ); }, info: Info( diff --git a/lib/fragments/dashboard/widgets/traffic_usage.dart b/lib/fragments/dashboard/widgets/traffic_usage.dart index aee3be1..3b073d3 100644 --- a/lib/fragments/dashboard/widgets/traffic_usage.dart +++ b/lib/fragments/dashboard/widgets/traffic_usage.dart @@ -51,10 +51,8 @@ class TrafficUsage extends StatelessWidget { @override Widget build(BuildContext context) { - final primaryColor = - context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2); - final secondaryColor = - context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3); + final primaryColor = globalState.theme.darken3PrimaryContainer; + final secondaryColor = globalState.theme.darken2SecondaryContainer; return SizedBox( height: getWidgetHeight(2), child: CommonCard( diff --git a/lib/fragments/hotkey.dart b/lib/fragments/hotkey.dart index e11f8bf..6feab46 100644 --- a/lib/fragments/hotkey.dart +++ b/lib/fragments/hotkey.dart @@ -4,6 +4,7 @@ import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/card.dart'; +import 'package:fl_clash/widgets/dialog.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -156,9 +157,28 @@ class _HotKeyRecorderState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(IntlExt.actionMessage((widget.hotKeyAction.action.name))), - content: ValueListenableBuilder( + return CommonDialog( + title: IntlExt.actionMessage(widget.hotKeyAction.action.name), + actions: [ + TextButton( + onPressed: () { + _handleRemove(); + }, + child: Text(appLocalizations.remove), + ), + const SizedBox( + width: 8, + ), + TextButton( + onPressed: () { + _handleConfirm(); + }, + child: Text( + appLocalizations.confirm, + ), + ), + ], + child: ValueListenableBuilder( valueListenable: hotKeyActionNotifier, builder: (_, hotKeyAction, ___) { final key = hotKeyAction.key; @@ -191,25 +211,6 @@ class _HotKeyRecorderState extends State { ); }, ), - actions: [ - TextButton( - onPressed: () { - _handleRemove(); - }, - child: Text(appLocalizations.remove), - ), - const SizedBox( - width: 8, - ), - TextButton( - onPressed: () { - _handleConfirm(); - }, - child: Text( - appLocalizations.confirm, - ), - ), - ], ); } } diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index 99be6bf..1333be0 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -22,8 +22,8 @@ class _LogsFragmentState extends ConsumerState with PageMixin { final _scrollController = ScrollController( initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, ); - final FixedMap _cacheDynamicHeightMap = FixedMap(1000); double _currentMaxWidth = 0; + final GlobalKey _key = GlobalKey(); List _logs = []; @@ -90,14 +90,13 @@ class _LogsFragmentState extends ConsumerState with PageMixin { void dispose() { _logsStateNotifier.dispose(); _scrollController.dispose(); - _cacheDynamicHeightMap.clear(); super.dispose(); } _handleTryClearCache(double maxWidth) { if (_currentMaxWidth != maxWidth) { _currentMaxWidth = maxWidth; - _cacheDynamicHeightMap.clear(); + _key.currentState?.clearCache(); } } @@ -116,27 +115,19 @@ class _LogsFragmentState extends ConsumerState with PageMixin { ); } - double _calcCacheHeight(String text) { - final cacheHeight = _cacheDynamicHeightMap.get(text); - if (cacheHeight != null) { - return cacheHeight; - } - final size = globalState.measure.computeTextSize( - Text( - text, - style: globalState.appController.context.textTheme.bodyLarge, - ), - maxWidth: _currentMaxWidth, - ); - _cacheDynamicHeightMap.put(text, size.height); - return size.height; - } - double _getItemHeight(Log log) { final measure = globalState.measure; final bodySmallHeight = measure.bodySmallHeight; final bodyMediumHeight = measure.bodyMediumHeight; - final height = _calcCacheHeight(log.payload ?? ""); + final height = globalState.measure + .computeTextSize( + Text( + log.payload ?? "", + style: globalState.appController.context.textTheme.bodyLarge, + ), + maxWidth: _currentMaxWidth, + ) + .height; return height + bodySmallHeight + 8 + bodyMediumHeight + 40; } @@ -196,7 +187,8 @@ class _LogsFragmentState extends ConsumerState with PageMixin { }, child: CommonScrollBar( controller: _scrollController, - child: ListView.builder( + child: CacheItemExtentListView( + key: _key, reverse: true, shrinkWrap: true, physics: NextClampingScrollPhysics(), @@ -204,7 +196,7 @@ class _LogsFragmentState extends ConsumerState with PageMixin { itemBuilder: (_, index) { return items[index]; }, - itemExtentBuilder: (index, __) { + itemExtentBuilder: (index) { final item = items[index]; if (item.runtimeType == Divider) { return 0; @@ -213,6 +205,14 @@ class _LogsFragmentState extends ConsumerState with PageMixin { 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 ?? ""; + }, ), ), ); @@ -272,11 +272,3 @@ class LogItem extends StatelessWidget { ); } } - -class NoGlowScrollBehavior extends ScrollBehavior { - @override - Widget buildOverscrollIndicator( - BuildContext context, Widget child, ScrollableDetails details) { - return child; // 禁用过度滚动效果 - } -} diff --git a/lib/fragments/profiles/add_profile.dart b/lib/fragments/profiles/add_profile.dart index 96e76b7..0f7a02f 100644 --- a/lib/fragments/profiles/add_profile.dart +++ b/lib/fragments/profiles/add_profile.dart @@ -50,19 +50,19 @@ class AddProfile extends StatelessWidget { return ListView( children: [ ListItem( - leading: const Icon(Icons.qr_code), + leading: const Icon(Icons.qr_code_sharp), title: Text(appLocalizations.qrcode), subtitle: Text(appLocalizations.qrcodeDesc), onTap: _toScan, ), ListItem( - leading: const Icon(Icons.upload_file), + leading: const Icon(Icons.upload_file_sharp), title: Text(appLocalizations.file), subtitle: Text(appLocalizations.fileDesc), onTap: _handleAddProfileFormFile, ), ListItem( - leading: const Icon(Icons.cloud_download), + leading: const Icon(Icons.cloud_download_sharp), title: Text(appLocalizations.url), subtitle: Text(appLocalizations.urlDesc), onTap: _toAdd, @@ -90,9 +90,15 @@ class _URLFormDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.importFromURL), - content: SizedBox( + return CommonDialog( + title: appLocalizations.importFromURL, + actions: [ + TextButton( + onPressed: _handleAddProfileFormURL, + child: Text(appLocalizations.submit), + ) + ], + child: SizedBox( width: 300, child: Wrap( runSpacing: 16, @@ -109,12 +115,6 @@ class _URLFormDialogState extends State { ], ), ), - actions: [ - TextButton( - onPressed: _handleAddProfileFormURL, - child: Text(appLocalizations.submit), - ) - ], ); } } diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 26c3292..1cee6bb 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -282,7 +282,7 @@ class _EditProfileState extends State { ValueListenableBuilder( valueListenable: fileInfoNotifier, builder: (_, fileInfo, __) { - return FadeBox( + return FadeThroughBox( child: fileInfo == null ? Container() : ListItem( @@ -324,15 +324,13 @@ class _EditProfileState extends State { }, ), ]; - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, __) { - if (didPop) return; + return CommonPopScope( + onPop: () { if (fileData == null) { - Navigator.of(context).pop(); - return; + return true; } _handleBack(); + return false; }, child: FloatLayout( floatingWidget: FloatWrapper( diff --git a/lib/fragments/profiles/gen_profile.dart b/lib/fragments/profiles/gen_profile.dart deleted file mode 100644 index aa0d9c1..0000000 --- a/lib/fragments/profiles/gen_profile.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:fl_clash/clash/core.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/card.dart'; -import 'package:fl_clash/widgets/scaffold.dart'; -import 'package:flutter/material.dart'; - -class GenProfile extends StatefulWidget { - final String profileId; - - const GenProfile({ - super.key, - required this.profileId, - }); - - @override - State createState() => _GenProfileState(); -} - -class _GenProfileState extends State { - final _currentClashConfigNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _initCurrentClashConfig(); - } - - _initCurrentClashConfig() async { - final currentProfileId = globalState.config.currentProfileId; - if (currentProfileId == null) { - return; - } - _currentClashConfigNotifier.value = - await clashCore.getProfile(currentProfileId); - } - - @override - Widget build(BuildContext context) { - return CommonScaffold( - body: ValueListenableBuilder( - valueListenable: _currentClashConfigNotifier, - builder: (_, clashConfig, ___) { - if (clashConfig == null) { - return Center( - child: CircularProgressIndicator(), - ); - } - return Padding( - padding: EdgeInsets.all(16), - child: CustomScrollView( - slivers: [ - SliverGrid.builder( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 100, - mainAxisExtent: 50, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), - itemCount: clashConfig.proxyGroups.length, - itemBuilder: (BuildContext context, int index) { - return CommonCard( - onPressed: () {}, - child: Text( - clashConfig.proxyGroups[index].name, - ), - ); - }, - ), - SliverList.builder( - itemBuilder: (BuildContext context, int index) { - final rule = clashConfig.rule[index]; - return Text( - rule, - ); - }, - itemCount: clashConfig.rule.length, - ) - ], - ), - ); - }, - ), - title: "自定义", - ); - } -} diff --git a/lib/fragments/profiles/override_profile.dart b/lib/fragments/profiles/override_profile.dart new file mode 100644 index 0000000..bb3d8fb --- /dev/null +++ b/lib/fragments/profiles/override_profile.dart @@ -0,0 +1,957 @@ +import 'dart:ui'; + +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/models.dart'; +import 'package:fl_clash/providers/providers.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'; + +class OverrideProfile extends StatefulWidget { + final String profileId; + + const OverrideProfile({ + super.key, + required this.profileId, + }); + + @override + State createState() => _OverrideProfileState(); +} + +class _OverrideProfileState extends State { + final GlobalKey _ruleListKey = GlobalKey(); + final _controller = ScrollController(); + double _currentMaxWidth = 0; + + _initState(WidgetRef ref) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(Duration(milliseconds: 300), () async { + final snippet = await clashCore.getProfile(widget.profileId); + final overrideData = ref.read( + getProfileOverrideDataProvider(widget.profileId), + ); + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith( + snippet: snippet, + overrideData: overrideData, + ), + ); + }); + }); + } + + _handleSave(WidgetRef ref, OverrideData overrideData) { + ref.read(profilesProvider.notifier).updateProfile( + widget.profileId, + (state) => state.copyWith( + overrideData: overrideData, + ), + ); + } + + _handleDelete(WidgetRef ref) async { + final res = await globalState.showMessage( + title: appLocalizations.tip, + message: TextSpan(text: appLocalizations.deleteRuleTip), + ); + if (res != true) { + return; + } + final selectedRules = ref.read( + profileOverrideStateProvider.select( + (state) => state.selectedRules, + ), + ); + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) { + final overrideRule = state.overrideData!.rule.updateRules( + (rules) => List.from( + rules.where( + (item) => !selectedRules.contains(item.id), + ), + ), + ); + return state.copyWith.overrideData!( + rule: overrideRule, + ); + }, + ); + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith(isEdit: false, selectedRules: {}), + ); + } + + _handleTryClearCache(double maxWidth) { + if (_currentMaxWidth != maxWidth) { + _currentMaxWidth = maxWidth; + _ruleListKey.currentState?.clearCache(); + } + } + + _buildContent() { + return Consumer( + builder: (_, ref, child) { + final isInit = ref.watch( + profileOverrideStateProvider.select( + (state) => state.snippet != null && state.overrideData != null, + ), + ); + if (!isInit) { + return Center( + child: CircularProgressIndicator(), + ); + } + return FadeBox( + child: !isInit + ? Center( + child: CircularProgressIndicator(), + ) + : child!, + ); + }, + child: LayoutBuilder( + builder: (_, constraints) { + _handleTryClearCache(constraints.maxWidth - 104); + return CommonAutoHiddenScrollBar( + controller: _controller, + child: CustomScrollView( + controller: _controller, + slivers: [ + SliverToBoxAdapter( + child: SizedBox( + height: 16, + ), + ), + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: OverrideSwitch(), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only( + left: 8, + right: 8, + ), + child: RuleTitle( + profileId: widget.profileId, + ), + ), + ), + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 0), + sliver: RuleContent( + maxWidth: _currentMaxWidth, + ruleListKey: _ruleListKey, + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: 16, + ), + ), + ], + ), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + profileOverrideStateProvider.overrideWith(() => ProfileOverrideState()), + ], + child: Consumer( + builder: (_, ref, child) { + _initState(ref); + return child!; + }, + child: Consumer( + builder: (_, ref, ___) { + final vm2 = ref.watch( + profileOverrideStateProvider.select( + (state) => VM2( + a: state.isEdit, + b: state.selectedRules.length, + ), + ), + ); + final isEdit = vm2.a; + final editCount = vm2.b; + return CommonScaffold( + title: appLocalizations.override, + body: _buildContent(), + actions: [ + if (!isEdit) + Consumer( + builder: (_, ref, child) { + final overrideData = ref.watch( + getProfileOverrideDataProvider(widget.profileId)); + final newOverrideData = ref.watch( + profileOverrideStateProvider.select( + (state) => state.overrideData, + ), + ); + final equals = overrideData == newOverrideData; + if (equals || newOverrideData == null) { + return SizedBox(); + } + return CommonPopScope( + onPop: () async { + if (equals) { + return true; + } + final res = await globalState.showMessage( + message: TextSpan( + text: appLocalizations.saveChanges, + ), + confirmText: appLocalizations.save, + ); + if (!context.mounted || res != true) { + return true; + } + _handleSave(ref, newOverrideData); + return true; + }, + child: IconButton( + onPressed: () async { + final res = await globalState.showMessage( + message: TextSpan( + text: appLocalizations.saveTip, + ), + confirmText: appLocalizations.tip, + ); + if (res != true) { + return; + } + _handleSave(ref, newOverrideData); + }, + icon: Icon( + Icons.save, + ), + ), + ); + }, + ), + if (editCount == 1) + IconButton( + onPressed: () { + final rule = ref.read(profileOverrideStateProvider.select( + (state) { + return state.overrideData?.rule.rules.firstWhere( + (item) => item.id == state.selectedRules.first, + ); + }, + )); + if (rule == null) { + return; + } + globalState.appController.handleAddOrUpdate( + ref, + rule, + ); + }, + icon: Icon( + Icons.edit, + ), + ), + if (editCount > 0) + IconButton( + onPressed: () { + _handleDelete(ref); + }, + icon: Icon( + Icons.delete, + ), + ) + ], + appBarEditState: AppBarEditState( + isEdit: isEdit, + editCount: editCount, + onExit: () { + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith( + isEdit: false, + selectedRules: {}, + ), + ); + }, + ), + ); + }, + ), + ), + ); + } +} + +class OverrideSwitch extends ConsumerWidget { + const OverrideSwitch({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final enable = ref.watch( + profileOverrideStateProvider.select( + (state) => state.overrideData?.enable, + ), + ); + return CommonCard( + onPressed: () {}, + type: CommonCardType.filled, + radius: 18, + child: ListItem.switchItem( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 4, + bottom: 4, + ), + title: Text(appLocalizations.enableOverride), + delegate: SwitchDelegate( + value: enable ?? false, + onChanged: (value) { + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith.overrideData!( + enable: value, + ), + ); + }, + ), + ), + ); + } +} + +class RuleTitle extends ConsumerWidget { + final String profileId; + + const RuleTitle({ + super.key, + required this.profileId, + }); + + _handleChangeType(WidgetRef ref, isOverrideRule) { + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith.overrideData!.rule( + type: isOverrideRule + ? OverrideRuleType.added + : OverrideRuleType.override, + ), + ); + } + + @override + Widget build(BuildContext context, ref) { + final vm3 = ref.watch( + profileOverrideStateProvider.select( + (state) { + final overrideRule = state.overrideData?.rule; + return VM3( + a: state.isEdit, + b: state.selectedRules.containsAll( + overrideRule?.rules.map((item) => item.id).toSet() ?? {}, + ), + c: overrideRule?.type == OverrideRuleType.override, + ); + }, + ), + ); + final isEdit = vm3.a; + final isSelectAll = vm3.b; + final isOverrideRule = vm3.c; + return FilledButtonTheme( + data: FilledButtonThemeData( + style: ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.symmetric( + horizontal: 8, + )), + visualDensity: VisualDensity.compact, + ), + ), + child: IconButtonTheme( + data: IconButtonThemeData( + style: ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.zero), + visualDensity: VisualDensity.compact, + iconSize: WidgetStatePropertyAll(20), + ), + ), + child: ListHeader( + title: appLocalizations.rule, + subTitle: isOverrideRule + ? appLocalizations.overrideOriginRules + : appLocalizations.addedOriginRules, + space: 8, + actions: [ + if (!isEdit) + IconButton.filledTonal( + icon: Icon( + isOverrideRule ? Icons.edit_document : Icons.note_add, + ), + onPressed: () { + _handleChangeType( + ref, + isOverrideRule, + ); + }, + ), + !isEdit + ? FilledButton.tonal( + onPressed: () { + globalState.appController.handleAddOrUpdate(ref); + }, + child: Text(appLocalizations.add), + ) + : isSelectAll + ? FilledButton( + onPressed: () { + ref + .read(profileOverrideStateProvider.notifier) + .updateState( + (state) => state.copyWith( + selectedRules: {}, + ), + ); + }, + child: Text(appLocalizations.selectAll), + ) + : FilledButton.tonal( + onPressed: () { + ref + .read(profileOverrideStateProvider.notifier) + .updateState( + (state) => state.copyWith( + selectedRules: state.overrideData?.rule.rules + .map((item) => item.id) + .toSet() ?? + {}, + ), + ); + }, + child: Text(appLocalizations.selectAll), + ), + ], + ), + ), + ); + } +} + +class RuleContent extends ConsumerWidget { + final Key ruleListKey; + final double maxWidth; + + const RuleContent({ + super.key, + required this.ruleListKey, + required this.maxWidth, + }); + + Widget _proxyDecorator( + Widget child, + int index, + Animation animation, + ) { + return AnimatedBuilder( + animation: animation, + builder: (_, Widget? child) { + final double animValue = Curves.easeInOut.transform(animation.value); + final double scale = lerpDouble(1, 1.02, animValue)!; + return Transform.scale( + scale: scale, + child: child, + ); + }, + child: child, + ); + } + + Widget _buildItem(Rule rule, int index) { + return Consumer( + builder: (context, ref, ___) { + final vm2 = ref.watch(profileOverrideStateProvider.select( + (item) => VM2( + a: item.isEdit, + b: item.selectedRules.contains(rule.id), + ), + )); + final isEdit = vm2.a; + final isSelected = vm2.b; + + return Material( + color: Colors.transparent, + child: Container( + margin: EdgeInsets.symmetric( + vertical: 4, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected + ? context.colorScheme.secondaryContainer.opacity80 + : context.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), + clipBehavior: Clip.hardEdge, + child: ListTile( + minTileHeight: 0, + minVerticalPadding: 0, + titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + trailing: SizedBox( + width: 24, + height: 24, + child: !isEdit + ? ReorderableDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), + ) + : CommonCheckBox( + value: isSelected, + isCircle: true, + onChanged: (_) { + _handleSelect(ref, rule); + }, + ), + ), + title: Text(rule.value), + ), + ), + ); + }, + ); + } + + _handleSelect(WidgetRef ref, ruleId) { + if (!ref.read(profileOverrideStateProvider).isEdit) { + return; + } + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) { + final newSelectedRules = Set.from(state.selectedRules); + if (newSelectedRules.contains(ruleId)) { + newSelectedRules.remove(ruleId); + } else { + newSelectedRules.add(ruleId); + } + return state.copyWith( + selectedRules: newSelectedRules, + ); + }, + ); + } + + @override + Widget build(BuildContext context, ref) { + final vm2 = ref.watch( + profileOverrideStateProvider.select( + (state) { + final overrideRule = state.overrideData?.rule; + return VM2( + a: overrideRule?.rules ?? [], + b: overrideRule?.type ?? OverrideRuleType.added, + ); + }, + ), + ); + final rules = vm2.a; + final type = vm2.b; + if (rules.isEmpty) { + return SliverToBoxAdapter( + child: SizedBox( + height: 300, + child: Center( + child: type == OverrideRuleType.added + ? Text( + appLocalizations.noData, + ) + : FilledButton( + onPressed: () { + final rules = ref.read( + profileOverrideStateProvider.select( + (state) => state.snippet?.rule ?? [], + ), + ); + ref + .read(profileOverrideStateProvider.notifier) + .updateState( + (state) { + return state.copyWith.overrideData!.rule( + overrideRules: rules, + ); + }, + ); + }, + child: Text(appLocalizations.getOriginRules), + ), + ), + ), + ); + } + return CacheItemExtentSliverReorderableList( + key: ruleListKey, + itemBuilder: (context, index) { + final rule = rules[index]; + return GestureDetector( + key: ObjectKey(rule), + child: _buildItem( + rule, + index, + ), + onTap: () { + _handleSelect(ref, rule.id); + }, + onLongPress: () { + if (ref.read(profileOverrideStateProvider).isEdit) { + return; + } + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith( + isEdit: true, + selectedRules: { + rule.id, + }, + ), + ); + }, + ); + }, + proxyDecorator: _proxyDecorator, + itemCount: rules.length, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final newRules = List.from(rules); + final item = newRules.removeAt(oldIndex); + newRules.insert(newIndex, item); + ref.read(profileOverrideStateProvider.notifier).updateState( + (state) => state.copyWith.overrideData!( + rule: state.overrideData!.rule.updateRules((_) => newRules), + ), + ); + }, + keyBuilder: (int index) { + return rules[index].value; + }, + itemExtentBuilder: (index) { + final rule = rules[index]; + return 40 + + globalState.measure + .computeTextSize( + Text( + rule.value, + style: context.textTheme.bodyMedium?.toJetBrainsMono, + ), + maxWidth: maxWidth, + ) + .height; + }, + ); + } +} + +class AddRuleDialog extends StatefulWidget { + final ClashConfigSnippet snippet; + final Rule? rule; + + const AddRuleDialog({ + super.key, + required this.snippet, + this.rule, + }); + + @override + State createState() => _AddRuleDialogState(); +} + +class _AddRuleDialogState extends State { + late RuleAction _ruleAction; + final _ruleTargetController = TextEditingController(); + final _contentController = TextEditingController(); + final _ruleProviderController = TextEditingController(); + final _subRuleController = TextEditingController(); + bool _noResolve = false; + bool _src = false; + List _targetItems = []; + List _ruleProviderItems = []; + List _subRuleItems = []; + final _formKey = GlobalKey(); + + @override + void initState() { + _initState(); + super.initState(); + } + + _initState() { + _targetItems = [ + ...widget.snippet.proxyGroups.map( + (item) => DropdownMenuEntry( + value: item.name, + label: item.name, + ), + ), + ...RuleTarget.values.map( + (item) => DropdownMenuEntry( + value: item.name, + label: item.name, + ), + ), + ]; + _ruleProviderItems = [ + ...widget.snippet.ruleProvider.map( + (item) => DropdownMenuEntry( + value: item.name, + label: item.name, + ), + ), + ]; + _subRuleItems = [ + ...widget.snippet.subRules.map( + (item) => DropdownMenuEntry( + value: item.name, + label: item.name, + ), + ), + ]; + if (widget.rule != null) { + final parsedRule = ParsedRule.parseString(widget.rule!.value); + _ruleAction = parsedRule.ruleAction; + _contentController.text = parsedRule.content ?? ""; + _ruleTargetController.text = parsedRule.ruleTarget ?? ""; + _ruleProviderController.text = parsedRule.ruleProvider ?? ""; + _subRuleController.text = parsedRule.subRule ?? ""; + _noResolve = parsedRule.noResolve; + _src = parsedRule.src; + return; + } + _ruleAction = RuleAction.values.first; + if (_targetItems.isNotEmpty) { + _ruleTargetController.text = _targetItems.first.value; + } + if (_ruleProviderItems.isNotEmpty) { + _ruleProviderController.text = _ruleProviderItems.first.value; + } + if (_subRuleItems.isNotEmpty) { + _subRuleController.text = _subRuleItems.first.value; + } + } + + @override + void didUpdateWidget(AddRuleDialog oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.rule != widget.rule) { + _initState(); + } + } + + _handleSubmit() { + final res = _formKey.currentState?.validate(); + if (res == false) { + return; + } + final parsedRule = ParsedRule( + ruleAction: _ruleAction, + content: _contentController.text, + ruleProvider: _ruleProviderController.text, + ruleTarget: _ruleTargetController.text, + subRule: _subRuleController.text, + noResolve: _noResolve, + src: _src, + ); + final rule = widget.rule != null + ? widget.rule!.copyWith(value: parsedRule.value) + : Rule.value( + parsedRule.value, + ); + Navigator.of(context).pop(rule); + } + + @override + Widget build(BuildContext context) { + return CommonDialog( + title: appLocalizations.addRule, + actions: [ + TextButton( + onPressed: _handleSubmit, + child: Text( + appLocalizations.confirm, + ), + ), + ], + child: DropdownMenuTheme( + data: DropdownMenuThemeData( + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder(), + labelStyle: context.textTheme.bodyLarge + ?.copyWith(overflow: TextOverflow.ellipsis), + ), + ), + child: Form( + key: _formKey, + child: LayoutBuilder( + builder: (_, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FilledButton.tonal( + onPressed: () async { + _ruleAction = + await globalState.showCommonDialog( + child: OptionsDialog( + title: appLocalizations.ruleName, + options: RuleAction.values, + textBuilder: (item) => item.value, + value: _ruleAction, + ), + ) ?? + _ruleAction; + setState(() {}); + }, + child: Text(_ruleAction.name), + ), + SizedBox( + height: 24, + ), + _ruleAction == RuleAction.RULE_SET + ? FormField( + validator: (_) { + if (_ruleProviderController.text.isEmpty) { + return appLocalizations.ruleProviderEmptyTip; + } + return null; + }, + builder: (field) { + return DropdownMenu( + expandedInsets: EdgeInsets.zero, + controller: _ruleProviderController, + label: Text(appLocalizations.ruleProviders), + menuHeight: 250, + errorText: field.errorText, + dropdownMenuEntries: _ruleProviderItems, + ); + }, + ) + : TextFormField( + controller: _contentController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: appLocalizations.content, + ), + validator: (_) { + if (_contentController.text.isEmpty) { + return appLocalizations.contentEmptyTip; + } + return null; + }, + ), + SizedBox( + height: 24, + ), + _ruleAction == RuleAction.SUB_RULE + ? FormField( + validator: (_) { + if (_subRuleController.text.isEmpty) { + return appLocalizations.subRuleEmptyTip; + } + return null; + }, + builder: (filed) { + return DropdownMenu( + width: 200, + controller: _subRuleController, + label: Text(appLocalizations.subRule), + menuHeight: 250, + dropdownMenuEntries: _subRuleItems, + ); + }, + ) + : FormField( + validator: (_) { + if (_ruleTargetController.text.isEmpty) { + return appLocalizations.ruleTargetEmptyTip; + } + return null; + }, + builder: (filed) { + return DropdownMenu( + controller: _ruleTargetController, + initialSelection: filed.value, + label: Text(appLocalizations.ruleTarget), + width: 200, + menuHeight: 250, + enableFilter: true, + dropdownMenuEntries: _targetItems, + errorText: filed.errorText, + ); + }, + ), + if (_ruleAction.hasParams) ...[ + SizedBox( + height: 20, + ), + Wrap( + spacing: 8, + children: [ + CommonCard( + radius: 8, + isSelected: _src, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 8, vertical: 8), + child: Text( + appLocalizations.sourceIp, + style: context.textTheme.bodyMedium, + ), + ), + onPressed: () { + setState(() { + _src = !_src; + }); + }, + ), + CommonCard( + radius: 8, + isSelected: _noResolve, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 8, vertical: 8), + child: Text( + appLocalizations.noResolve, + style: context.textTheme.bodyMedium, + ), + ), + onPressed: () { + setState(() { + _noResolve = !_noResolve; + }); + }, + ) + ], + ), + ], + SizedBox( + height: 20, + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 7e0d8bf..2934899 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart'; +import 'package:fl_clash/fragments/profiles/override_profile.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; @@ -11,7 +12,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'add_profile.dart'; -import 'gen_profile.dart'; class ProfilesFragment extends StatefulWidget { const ProfilesFragment({super.key}); @@ -24,12 +24,17 @@ class _ProfilesFragmentState extends State with PageMixin { Function? applyConfigDebounce; _handleShowAddExtendPage() { - showExtendPage( + showExtend( globalState.navigatorKey.currentState!.context, - body: AddProfile( - context: globalState.navigatorKey.currentState!.context, - ), - title: "${appLocalizations.add}${appLocalizations.profile}", + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: AddProfile( + context: globalState.navigatorKey.currentState!.context, + ), + title: "${appLocalizations.add}${appLocalizations.profile}", + ); + }, ); } @@ -81,12 +86,13 @@ class _ProfilesFragmentState extends State with PageMixin { onPressed: () { final profiles = globalState.config.profiles; showSheet( - title: appLocalizations.profilesSort, context: context, - body: SizedBox( - height: 400, - child: ReorderableProfiles(profiles: profiles), - ), + builder: (_, type) { + return ReorderableProfilesSheet( + type: type, + profiles: profiles, + ); + }, ); }, icon: const Icon(Icons.sort), @@ -205,13 +211,18 @@ class ProfileItem extends StatelessWidget { } _handleShowEditExtendPage(BuildContext context) { - showExtendPage( + showExtend( context, - body: EditProfile( - profile: profile, - context: context, - ), - title: "${appLocalizations.edit}${appLocalizations.profile}", + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: EditProfile( + profile: profile, + context: context, + ), + title: "${appLocalizations.edit}${appLocalizations.profile}", + ); + }, ); } @@ -277,7 +288,7 @@ class ProfileItem extends StatelessWidget { _handlePushGenProfilePage(BuildContext context, String id) { BaseNavigator.push( context, - GenProfile( + OverrideProfile( profileId: id, ), ); @@ -285,7 +296,6 @@ class ProfileItem extends StatelessWidget { @override Widget build(BuildContext context) { - final key = GlobalKey(); return CommonCard( isSelected: profile.id == groupValue, onPressed: () { @@ -298,17 +308,16 @@ class ProfileItem extends StatelessWidget { trailing: SizedBox( height: 40, width: 40, - child: FadeBox( + child: FadeThroughBox( child: profile.isUpdating ? const Padding( padding: EdgeInsets.all(8), child: CircularProgressIndicator(), ) : CommonPopupBox( - key: key, popup: CommonPopupMenu( items: [ - ActionItemData( + PopupMenuItemData( icon: Icons.edit_outlined, label: appLocalizations.edit, onPressed: () { @@ -316,52 +325,47 @@ class ProfileItem extends StatelessWidget { }, ), if (profile.type == ProfileType.url) ...[ - ActionItemData( + PopupMenuItemData( icon: Icons.sync_alt_sharp, label: appLocalizations.sync, onPressed: () { updateProfile(); }, ), - // ActionItemData( - // icon: Icons.copy, - // label: appLocalizations.copyLink, - // onPressed: () { - // _handleCopyLink(context); - // }, - // ), ], - // ActionItemData( - // icon: Icons.extension_outlined, - // label: "自定义", - // onPressed: () { - // _handlePushGenProfilePage(context, profile.id); - // }, - // ), - ActionItemData( + PopupMenuItemData( + icon: Icons.extension_outlined, + label: appLocalizations.override, + onPressed: () { + _handlePushGenProfilePage(context, profile.id); + }, + ), + PopupMenuItemData( icon: Icons.file_copy_outlined, label: appLocalizations.exportFile, onPressed: () { _handleExportFile(context); }, ), - ActionItemData( + PopupMenuItemData( icon: Icons.delete_outlined, iconSize: 20, label: appLocalizations.delete, onPressed: () { _handleDeleteProfile(context); }, - type: ActionType.danger, + type: PopupMenuItemType.danger, ), ], ), - target: IconButton( - onPressed: () { - key.currentState?.pop(); - }, - icon: Icon(Icons.more_vert), - ), + targetBuilder: (open) { + return IconButton( + onPressed: () { + open(); + }, + icon: Icon(Icons.more_vert), + ); + }, ), ), ), @@ -397,19 +401,22 @@ class ProfileItem extends StatelessWidget { } } -class ReorderableProfiles extends StatefulWidget { +class ReorderableProfilesSheet extends StatefulWidget { final List profiles; + final SheetType type; - const ReorderableProfiles({ + const ReorderableProfilesSheet({ super.key, required this.profiles, + required this.type, }); @override - State createState() => _ReorderableProfilesState(); + State createState() => + _ReorderableProfilesSheetState(); } -class _ReorderableProfilesState extends State { +class _ReorderableProfilesSheetState extends State { late List profiles; @override @@ -453,74 +460,61 @@ class _ReorderableProfilesState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - flex: 1, - child: ReorderableListView.builder( - buildDefaultDragHandles: false, - padding: const EdgeInsets.symmetric(horizontal: 12), - proxyDecorator: proxyDecorator, - onReorder: (oldIndex, newIndex) { - setState(() { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final profile = profiles.removeAt(oldIndex); - profiles.insert(newIndex, profile); - }); - }, - itemBuilder: (_, index) { - final profile = profiles[index]; - return Container( - key: Key(profile.id), - padding: const EdgeInsets.symmetric(vertical: 4), - child: CommonCard( - type: CommonCardType.filled, - child: ListTile( - contentPadding: const EdgeInsets.only( - right: 16, - left: 16, - ), - title: Text(profile.label ?? profile.id), - trailing: ReorderableDragStartListener( - index: index, - child: const Icon(Icons.drag_handle), - ), + return AdaptiveSheetScaffold( + type: widget.type, + actions: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + globalState.appController.setProfiles(profiles); + }, + icon: Icon( + Icons.save, + ), + ) + ], + body: Padding( + padding: EdgeInsets.only(bottom: 32), + child: ReorderableListView.builder( + buildDefaultDragHandles: false, + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + proxyDecorator: proxyDecorator, + onReorder: (oldIndex, newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final profile = profiles.removeAt(oldIndex); + profiles.insert(newIndex, profile); + }); + }, + itemBuilder: (_, index) { + final profile = profiles[index]; + return Container( + key: Key(profile.id), + padding: const EdgeInsets.symmetric(vertical: 4), + child: CommonCard( + type: CommonCardType.filled, + child: ListTile( + contentPadding: const EdgeInsets.only( + right: 16, + left: 16, + ), + title: Text(profile.label ?? profile.id), + trailing: ReorderableDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), ), ), - ); - }, - itemCount: profiles.length, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 24, - ), - child: FilledButton.tonal( - onPressed: () { - Navigator.of(context).pop(); - globalState.appController.setProfiles(profiles); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 8), ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - appLocalizations.confirm, - ), - ], - ), - ), + ); + }, + itemCount: profiles.length, ), - ], + ), + title: appLocalizations.profilesSort, ); } } diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index a6020d4..4433335 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -178,7 +178,7 @@ class ProxyCard extends StatelessWidget { style: context.textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis, color: - context.textTheme.bodySmall?.color?.toLight, + context.textTheme.bodySmall?.color?.opacity80, ), ), ), @@ -221,7 +221,7 @@ class _ProxyDesc extends ConsumerWidget { desc, overflow: TextOverflow.ellipsis, style: context.textTheme.bodySmall?.copyWith( - color: context.textTheme.bodySmall?.color?.toLight, + color: context.textTheme.bodySmall?.color?.opacity80, ), ); } diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index 324d3e8..9612a60 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -467,10 +467,6 @@ class _ListHeaderState extends State return CommonCard( enterAnimated: widget.enterAnimated, key: widget.key, - borderSide: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll( - context.colorScheme.surfaceContainer, - ), radius: 14, type: CommonCardType.filled, child: Padding( @@ -556,6 +552,7 @@ class _ListHeaderState extends State children: [ if (isExpand) ...[ IconButton( + visualDensity: VisualDensity.standard, onPressed: () { widget.onScrollToSelected(groupName); }, @@ -565,12 +562,13 @@ class _ListHeaderState extends State ), IconButton( onPressed: _delayTest, + visualDensity: VisualDensity.standard, icon: const Icon( Icons.network_ping, ), ), const SizedBox( - width: 4, + width: 6, ), ], AnimatedBuilder( diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart index 95855ea..22ce8a2 100644 --- a/lib/fragments/proxies/providers.dart +++ b/lib/fragments/proxies/providers.dart @@ -13,8 +13,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; typedef UpdatingMap = Map; class ProvidersView extends ConsumerStatefulWidget { + final SheetType type; + const ProvidersView({ super.key, + required this.type, }); @override @@ -22,25 +25,6 @@ class ProvidersView extends ConsumerStatefulWidget { } class _ProvidersViewState extends ConsumerState { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - globalState.appController.updateProviders(); - context.commonScaffoldState?.actions = [ - IconButton( - onPressed: () { - _updateProviders(); - }, - icon: const Icon( - Icons.sync, - ), - ) - ]; - }, - ); - } _updateProviders() async { final providers = ref.read(providersProvider); @@ -102,10 +86,24 @@ class _ProvidersViewState extends ConsumerState { title: appLocalizations.ruleProviders, items: ruleProviders, ); - return generateListView([ - ...proxySection, - ...ruleSection, - ]); + return AdaptiveSheetScaffold( + actions: [ + IconButton( + onPressed: () { + _updateProviders(); + }, + icon: const Icon( + Icons.sync, + ), + ) + ], + type: widget.type, + body: generateListView([ + ...proxySection, + ...ruleSection, + ]), + title: appLocalizations.providers, + ); } } @@ -222,7 +220,7 @@ class ProviderItem extends StatelessWidget { trailing: SizedBox( height: 48, width: 48, - child: FadeBox( + child: FadeThroughBox( child: provider.isUpdating ? const Padding( padding: EdgeInsets.all(8), diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index b414ccc..d454726 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -28,12 +28,13 @@ class _ProxiesFragmentState extends ConsumerState if (_hasProviders) IconButton( onPressed: () { - showExtendPage( - isScaffold: true, - extendPageWidth: 360, + showExtend( context, - body: const ProvidersView(), - title: appLocalizations.providers, + builder: (_, type) { + return ProvidersView( + type: type, + ); + }, ); }, icon: const Icon( @@ -51,11 +52,15 @@ class _ProxiesFragmentState extends ConsumerState ) : IconButton( onPressed: () { - showExtendPage( + showExtend( context, - extendPageWidth: 360, - title: appLocalizations.iconConfiguration, - body: _IconConfigView(), + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: const _IconConfigView(), + title: appLocalizations.iconConfiguration, + ); + }, ); }, icon: const Icon( @@ -65,9 +70,17 @@ class _ProxiesFragmentState extends ConsumerState IconButton( onPressed: () { showSheet( - title: appLocalizations.proxiesSetting, context: context, - body: const ProxiesSetting(), + props: SheetProps( + isScrollControlled: true, + ), + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: const ProxiesSetting(), + title: appLocalizations.proxiesSetting, + ); + }, ); }, icon: const Icon( @@ -128,13 +141,11 @@ class _IconConfigView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final iconMap = ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap)); - final entries = iconMap.entries.toList(); - return ListPage( + return MapInputPage( title: appLocalizations.iconConfiguration, - items: entries, + map: iconMap, keyLabel: appLocalizations.regExp, valueLabel: appLocalizations.icon, - keyBuilder: (item) => Key(item.key), titleBuilder: (item) => Text(item.key), leadingBuilder: (item) => Container( decoration: BoxDecoration( @@ -151,10 +162,10 @@ class _IconConfigView extends ConsumerWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - onChange: (entries) { + onChange: (value) { ref.read(proxiesStyleSettingProvider.notifier).updateState( (state) => state.copyWith( - iconMap: Map.fromEntries(entries), + iconMap: value, ), ); }, diff --git a/lib/fragments/proxies/setting.dart b/lib/fragments/proxies/setting.dart index a654eb3..c2cc09f 100644 --- a/lib/fragments/proxies/setting.dart +++ b/lib/fragments/proxies/setting.dart @@ -248,8 +248,8 @@ class ProxiesSetting extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 32), + return SingleChildScrollView( + padding: EdgeInsets.only(bottom: 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -268,6 +268,7 @@ class ProxiesSetting extends StatelessWidget { return Container(); }, child: Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ ..._buildGroupStyleSetting(), diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index b476098..9d29656 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -49,7 +49,7 @@ class ProxiesTabFragmentState extends ConsumerState _buildMoreButton() { return Consumer( builder: (_, ref, ___) { - final isMobileView = ref.watch(viewWidthProvider.notifier).isMobileView; + final isMobileView = ref.watch(isMobileViewProvider); return IconButton( onPressed: _showMoreMenu, icon: isMobileView @@ -67,42 +67,48 @@ class ProxiesTabFragmentState extends ConsumerState _showMoreMenu() { showSheet( context: context, - width: 380, - isScrollControlled: false, - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Consumer( - builder: (_, ref, __) { - final state = ref.watch(proxiesSelectorStateProvider); - return SizedBox( - width: double.infinity, - child: Wrap( - alignment: WrapAlignment.center, - runSpacing: 8, - spacing: 8, - children: [ - for (final groupName in state.groupNames) - SettingTextCard( - groupName, - onPressed: () { - final index = state.groupNames.indexWhere( - (item) => item == groupName, - ); - if (index == -1) return; - _tabController?.animateTo(index); - globalState.appController - .updateCurrentGroupName(groupName); - Navigator.of(context).pop(); - }, - isSelected: groupName == state.currentGroupName, - ) - ], - ), - ); - }, - ), + props: SheetProps( + isScrollControlled: false, ), - title: appLocalizations.proxyGroup, + builder: (_, type) { + return AdaptiveSheetScaffold( + type: type, + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Consumer( + builder: (_, ref, __) { + final state = ref.watch(proxiesSelectorStateProvider); + return SizedBox( + width: double.infinity, + child: Wrap( + alignment: WrapAlignment.center, + runSpacing: 8, + spacing: 8, + children: [ + for (final groupName in state.groupNames) + SettingTextCard( + groupName, + onPressed: () { + final index = state.groupNames.indexWhere( + (item) => item == groupName, + ); + if (index == -1) return; + _tabController?.animateTo(index); + globalState.appController + .updateCurrentGroupName(groupName); + Navigator.of(context).pop(); + }, + isSelected: groupName == state.currentGroupName, + ) + ], + ), + ); + }, + ), + ), + title: appLocalizations.proxyGroup, + ); + }, ); } @@ -238,7 +244,7 @@ class ProxiesTabFragmentState extends ConsumerState begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ - context.colorScheme.surface.withOpacity(0.1), + context.colorScheme.surface.opacity10, context.colorScheme.surface, ], stops: const [ @@ -319,32 +325,35 @@ class ProxyGroupViewState extends ConsumerState { ); return Align( alignment: Alignment.topCenter, - child: GridView.builder( + child: CommonAutoHiddenScrollBar( controller: _controller, - padding: const EdgeInsets.only( - top: 16, - left: 16, - right: 16, - bottom: 96, + child: GridView.builder( + controller: _controller, + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + bottom: 96, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: getItemHeight(proxyCardType), + ), + itemCount: sortedProxies.length, + itemBuilder: (_, index) { + final proxy = sortedProxies[index]; + return ProxyCard( + testUrl: state.testUrl, + groupType: state.groupType, + type: proxyCardType, + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ); + }, ), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: getItemHeight(proxyCardType), - ), - itemCount: sortedProxies.length, - itemBuilder: (_, index) { - final proxy = sortedProxies[index]; - return ProxyCard( - testUrl: state.testUrl, - groupType: state.groupType, - type: proxyCardType, - key: ValueKey('$groupName.${proxy.name}'), - proxy: proxy, - groupName: groupName, - ); - }, ), ); } diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index 694bc91..22b7508 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -149,7 +149,7 @@ class _GeoDataListItemState extends State { builder: (_, snapshot) { return SizedBox( height: 24, - child: FadeBox( + child: FadeThroughBox( key: Key("fade_box_${geoItem.label}"), child: snapshot.data == null ? const SizedBox( @@ -248,7 +248,7 @@ class _GeoDataListItemState extends State { child: ValueListenableBuilder( valueListenable: isUpdating, builder: (_, isUpdating, ___) { - return FadeBox( + return FadeThroughBox( child: isUpdating ? const Padding( padding: EdgeInsets.all(8), @@ -299,24 +299,8 @@ class _UpdateGeoUrlFormDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(widget.title), - content: SizedBox( - width: 300, - child: Wrap( - runSpacing: 16, - children: [ - TextField( - maxLines: 5, - minLines: 1, - controller: urlController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - ), - ], - ), - ), + return CommonDialog( + title: widget.title, actions: [ if (widget.defaultValue != null && urlController.value.text != widget.defaultValue) ...[ @@ -333,6 +317,19 @@ class _UpdateGeoUrlFormDialogState extends State { child: Text(appLocalizations.submit), ) ], + child: Wrap( + runSpacing: 16, + children: [ + TextField( + maxLines: 5, + minLines: 1, + controller: urlController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ], + ), ); } } diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index d20f0e0..1d62d31 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -291,12 +291,12 @@ class _PrimaryColorItem extends ConsumerWidget { itemBuilder: (_, index) { final color = primaryColors[index]; return ColorSchemeBox( - isSelected: color?.value == primaryColor, + isSelected: color?.toARGB32() == primaryColor, primaryColor: color, onPressed: () { ref.read(themeSettingProvider.notifier).updateState( (state) => state.copyWith( - primaryColor: color?.value, + primaryColor: color?.toARGB32(), ), ); }, diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart index 9a04a48..9f85d6d 100644 --- a/lib/fragments/tools.dart +++ b/lib/fragments/tools.dart @@ -35,7 +35,6 @@ class _ToolboxFragmentState extends ConsumerState { delegate: OpenDelegate( title: Intl.message(navigationItem.label.name), widget: navigationItem.fragment, - extendPageWidth: 360, ), ); } @@ -65,7 +64,7 @@ class _ToolboxFragmentState extends ConsumerState { ); } - List _getSettingList() { + _getSettingList() { return generateSection( title: appLocalizations.settings, items: [ @@ -75,7 +74,7 @@ class _ToolboxFragmentState extends ConsumerState { if (system.isDesktop) _HotkeyItem(), if (Platform.isWindows) _LoopbackItem(), if (Platform.isAndroid) _AccessItem(), - _OverrideItem(), + _ConfigItem(), _SettingItem(), ], ); @@ -155,7 +154,6 @@ class _ThemeItem extends StatelessWidget { delegate: OpenDelegate( title: appLocalizations.theme, widget: const ThemeFragment(), - extendPageWidth: 360, ), ); } @@ -231,15 +229,15 @@ class _AccessItem extends StatelessWidget { } } -class _OverrideItem extends StatelessWidget { - const _OverrideItem(); +class _ConfigItem extends StatelessWidget { + const _ConfigItem(); @override Widget build(BuildContext context) { return ListItem.open( leading: const Icon(Icons.edit), - title: Text(appLocalizations.override), - subtitle: Text(appLocalizations.overrideDesc), + title: Text(appLocalizations.basicConfig), + subtitle: Text(appLocalizations.basicConfigDesc), delegate: OpenDelegate( title: appLocalizations.override, widget: const ConfigFragment(), diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index b8d4642..d21add9 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -123,7 +123,6 @@ "project": "Project", "core": "Core", "tabAnimation": "Tab animation", - "tabAnimationDesc": "When enabled, the home tab will add a toggle animation", "desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", "startVpn": "Starting VPN...", "stopVpn": "Stopping VPN...", @@ -181,7 +180,6 @@ "requests": "Requests", "requestsDesc": "View recently request records", "findProcessMode": "Find process", - "findProcessModeDesc": "There is a risk of flashback after opening", "init": "Init", "infiniteTime": "Long term effective", "expirationTime": "Expiration time", @@ -249,7 +247,6 @@ "stop": "Stop", "appDesc": "Processing app related settings", "vpnDesc": "Modify VPN related settings", - "generalDesc": "Overwrite general settings", "dnsDesc": "Update DNS related settings", "key": "Key", "value": "Value", @@ -346,5 +343,34 @@ "exportFile": "Export file", "cacheCorrupt": "The cache is corrupt. Do you want to clear it?", "detectionTip": "Relying on third-party api is for reference only", - "listen": "Listen" + "listen": "Listen", + "keyExists": "The current key already exists", + "valueExists": "The current value already exists", + "undo": "undo", + "redo": "redo", + "none": "none", + "basicConfig": "Basic configuration", + "basicConfigDesc": "Modify the basic configuration globally", + "selectedCountTitle": "{count} items have been selected", + "addRule": "Add rule", + "ruleProviderEmptyTip": "Rule provider cannot be empty", + "ruleName": "Rule name", + "content": "Content", + "contentEmptyTip": "Content cannot be empty", + "subRule": "Sub rule", + "subRuleEmptyTip": "Sub rule content cannot be empty", + "ruleTarget": "Rule target", + "ruleTargetEmptyTip": "Rule target cannot be empty", + "sourceIp": "Source IP", + "noResolve": "No resolve IP", + "getOriginRules": "Get original rules", + "overrideOriginRules": "Override the original rule", + "addedOriginRules": "Attach on the original rules", + "enableOverride": "Enable override", + "deleteRuleTip": "Are you sure you want to delete the selected rule?", + "saveChanges": "Do you want to save the changes?", + "generalDesc": "Modify general settings", + "findProcessModeDesc": "There is a certain performance loss after opening", + "tabAnimationDesc": "Effective only in mobile view", + "saveTip": "Are you sure you want to save?" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_ja.arb b/lib/l10n/arb/intl_ja.arb index aac5802..7c64a38 100644 --- a/lib/l10n/arb/intl_ja.arb +++ b/lib/l10n/arb/intl_ja.arb @@ -123,7 +123,6 @@ "project": "プロジェクト", "core": "コア", "tabAnimation": "タブアニメーション", - "tabAnimationDesc": "有効化するとホームタブに切り替えアニメーションを追加", "desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。", "startVpn": "VPNを開始中...", "stopVpn": "VPNを停止中...", @@ -181,7 +180,6 @@ "requests": "リクエスト", "requestsDesc": "最近のリクエスト記録を表示", "findProcessMode": "プロセス検出", - "findProcessModeDesc": "有効化するとフラッシュバックのリスクあり", "init": "初期化", "infiniteTime": "長期有効", "expirationTime": "有効期限", @@ -249,7 +247,6 @@ "stop": "停止", "appDesc": "アプリ関連設定の処理", "vpnDesc": "VPN関連設定の変更", - "generalDesc": "一般設定の上書き", "dnsDesc": "DNS関連設定の更新", "key": "キー", "value": "値", @@ -346,5 +343,34 @@ "exportFile": "ファイルをエクスポート", "cacheCorrupt": "キャッシュが破損しています。クリアしますか?", "detectionTip": "サードパーティAPIに依存(参考値)", - "listen": "リスン" + "listen": "リスン", + "keyExists": "現在のキーは既に存在します", + "valueExists": "現在の値は既に存在します", + "undo": "元に戻す", + "redo": "やり直す", + "none": "なし", + "basicConfig": "基本設定", + "basicConfigDesc": "基本設定をグローバルに変更", + "selectedCountTitle": "{count} 項目が選択されています", + "addRule": "ルールを追加", + "ruleProviderEmptyTip": "ルールプロバイダーは必須です", + "ruleName": "ルール名", + "content": "内容", + "contentEmptyTip": "内容は必須です", + "subRule": "サブルール", + "subRuleEmptyTip": "サブルールの内容は必須です", + "ruleTarget": "ルール対象", + "ruleTargetEmptyTip": "ルール対象は必須です", + "sourceIp": "送信元IP", + "noResolve": "IPを解決しない", + "getOriginRules": "元のルールを取得", + "overrideOriginRules": "元のルールを上書き", + "addedOriginRules": "元のルールに追加", + "enableOverride": "上書きを有効化", + "deleteRuleTip": "選択したルールを削除しますか?", + "saveChanges": "変更を保存しますか?", + "generalDesc": "一般設定を変更", + "findProcessModeDesc": "有効化するとパフォーマンスが若干低下します", + "tabAnimationDesc": "モバイル表示でのみ有効", + "saveTip": "保存してもよろしいですか?" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_ru.arb b/lib/l10n/arb/intl_ru.arb index 7971818..5f40f90 100644 --- a/lib/l10n/arb/intl_ru.arb +++ b/lib/l10n/arb/intl_ru.arb @@ -123,7 +123,6 @@ "project": "Проект", "core": "Ядро", "tabAnimation": "Анимация вкладок", - "tabAnimationDesc": "При включении домашняя вкладка добавит анимацию переключения", "desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.", "startVpn": "Запуск VPN...", "stopVpn": "Остановка VPN...", @@ -181,7 +180,6 @@ "requests": "Запросы", "requestsDesc": "Просмотр последних записей запросов", "findProcessMode": "Режим поиска процесса", - "findProcessModeDesc": "Есть риск сбоя после включения", "init": "Инициализация", "infiniteTime": "Долгосрочное действие", "expirationTime": "Время истечения", @@ -249,7 +247,6 @@ "stop": "Стоп", "appDesc": "Обработка настроек, связанных с приложением", "vpnDesc": "Изменение настроек, связанных с VPN", - "generalDesc": "Переопределение общих настроек", "dnsDesc": "Обновление настроек, связанных с DNS", "key": "Ключ", "value": "Значение", @@ -346,5 +343,34 @@ "exportFile": "Экспорт файла", "cacheCorrupt": "Кэш поврежден. Хотите очистить его?", "detectionTip": "Опирается на сторонний API, только для справки", - "listen": "Слушать" + "listen": "Слушать", + "keyExists": "Текущий ключ уже существует", + "valueExists": "Текущее значение уже существует", + "undo": "Отменить", + "redo": "Повторить", + "none": "Нет", + "basicConfig": "Базовая конфигурация", + "basicConfigDesc": "Глобальное изменение базовых настроек", + "selectedCountTitle": "Выбрано {count} элементов", + "addRule": "Добавить правило", + "ruleProviderEmptyTip": "Поставщик правил не может быть пустым", + "ruleName": "Название правила", + "content": "Содержание", + "contentEmptyTip": "Содержание не может быть пустым", + "subRule": "Подправило", + "subRuleEmptyTip": "Содержание подправила не может быть пустым", + "ruleTarget": "Цель правила", + "ruleTargetEmptyTip": "Цель правила не может быть пустой", + "sourceIp": "Исходный IP", + "noResolve": "Не разрешать IP", + "getOriginRules": "Получить оригинальные правила", + "overrideOriginRules": "Переопределить оригинальное правило", + "addedOriginRules": "Добавить к оригинальным правилам", + "enableOverride": "Включить переопределение", + "deleteRuleTip": "Вы уверены, что хотите удалить выбранное правило?", + "saveChanges": "Сохранить изменения?", + "generalDesc": "Изменение общих настроек", + "findProcessModeDesc": "При включении возможны небольшие потери производительности", + "tabAnimationDesc": "Действительно только в мобильном виде", + "saveTip": "Вы уверены, что хотите сохранить?" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 78950e7..7b32e0b 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -123,7 +123,6 @@ "project": "项目", "core": "内核", "tabAnimation": "选项卡动画", - "tabAnimationDesc": "开启后,主页选项卡将添加切换动画", "desc": "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。", "startVpn": "正在启动VPN...", "stopVpn": "正在停止VPN...", @@ -169,7 +168,7 @@ "externalControllerDesc": "开启后将可以通过9090端口控制Clash内核", "ipv6Desc": "开启后将可以接收IPv6流量", "app": "应用", - "general": "基础", + "general": "常规", "vpnSystemProxyDesc": "为VpnService附加HTTP代理", "systemProxyDesc": "设置系统代理", "unifiedDelay": "统一延迟", @@ -181,7 +180,6 @@ "requests": "请求", "requestsDesc": "查看最近请求记录", "findProcessMode": "查找进程", - "findProcessModeDesc": "开启后存在闪退风险", "init": "初始化", "infiniteTime": "长期有效", "expirationTime": "到期时间", @@ -249,7 +247,6 @@ "stop": "暂停", "appDesc": "处理应用相关设置", "vpnDesc": "修改VPN相关设置", - "generalDesc": "覆写基础设置", "dnsDesc": "更新DNS相关设置", "key": "键", "value": "值", @@ -346,5 +343,34 @@ "exportFile": "导出文件", "cacheCorrupt": "缓存已损坏,是否清空?", "detectionTip": "依赖第三方api,仅供参考", - "listen": "监听" + "listen": "监听", + "keyExists": "当前键已存在", + "valueExists": "当前值已存在", + "undo": "撤销", + "redo": "重做", + "none": "无", + "basicConfig": "基本配置", + "basicConfigDesc": "全局修改基本配置", + "selectedCountTitle": "已选择 {count} 项", + "addRule": "添加规则", + "ruleProviderEmptyTip": "规则提供者不能为空", + "ruleName": "规则名称", + "content": "内容", + "contentEmptyTip": "内容不能为空", + "subRule": "子规则", + "subRuleEmptyTip": "子规则内容不能为空", + "ruleTarget": "规则目标", + "ruleTargetEmptyTip": "规则目标不能为空", + "sourceIp": "源IP", + "noResolve": "不解析IP", + "getOriginRules": "获取原始规则", + "overrideOriginRules": "覆盖原始规则", + "addedOriginRules": "附加到原始规则", + "enableOverride": "启用覆写", + "deleteRuleTip": "确定要删除选中的规则吗?", + "saveChanges": "是否保存更改?", + "generalDesc": "修改通用设置", + "findProcessModeDesc": "开启后会有一定性能损耗", + "tabAnimationDesc": "仅在移动视图中有效", + "saveTip": "确定要保存吗?" } diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 6a83638..8b63e4b 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -20,6 +20,8 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; + static String m0(count) => "${count} items have been selected"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About"), @@ -44,6 +46,10 @@ class MessageLookup extends MessageLookupByLibrary { "action_tun": MessageLookupByLibrary.simpleMessage("TUN"), "action_view": MessageLookupByLibrary.simpleMessage("Show/Hide"), "add": MessageLookupByLibrary.simpleMessage("Add"), + "addRule": MessageLookupByLibrary.simpleMessage("Add rule"), + "addedOriginRules": MessageLookupByLibrary.simpleMessage( + "Attach on the original rules", + ), "address": MessageLookupByLibrary.simpleMessage("Address"), "addressHelp": MessageLookupByLibrary.simpleMessage( "WebDAV server address", @@ -114,6 +120,10 @@ class MessageLookup extends MessageLookupByLibrary { "Sync data via WebDAV or file", ), "backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"), + "basicConfig": MessageLookupByLibrary.simpleMessage("Basic configuration"), + "basicConfigDesc": MessageLookupByLibrary.simpleMessage( + "Modify the basic configuration globally", + ), "bind": MessageLookupByLibrary.simpleMessage("Bind"), "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"), "bypassDomain": MessageLookupByLibrary.simpleMessage("Bypass domain"), @@ -149,6 +159,10 @@ class MessageLookup extends MessageLookupByLibrary { "View current connections data", ), "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"), + "content": MessageLookupByLibrary.simpleMessage("Content"), + "contentEmptyTip": MessageLookupByLibrary.simpleMessage( + "Content cannot be empty", + ), "copy": MessageLookupByLibrary.simpleMessage("Copy"), "copyEnvVar": MessageLookupByLibrary.simpleMessage( "Copying environment variables", @@ -177,6 +191,9 @@ class MessageLookup extends MessageLookupByLibrary { "deleteProfileTip": MessageLookupByLibrary.simpleMessage( "Sure you want to delete the current profile?", ), + "deleteRuleTip": MessageLookupByLibrary.simpleMessage( + "Are you sure you want to delete the selected rule?", + ), "desc": MessageLookupByLibrary.simpleMessage( "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", ), @@ -205,6 +222,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("Download"), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "en": MessageLookupByLibrary.simpleMessage("English"), + "enableOverride": MessageLookupByLibrary.simpleMessage("Enable override"), "entries": MessageLookupByLibrary.simpleMessage(" entries"), "exclude": MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"), "excludeDesc": MessageLookupByLibrary.simpleMessage( @@ -243,13 +261,13 @@ class MessageLookup extends MessageLookupByLibrary { ), "findProcessMode": MessageLookupByLibrary.simpleMessage("Find process"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage( - "There is a risk of flashback after opening", + "There is a certain performance loss after opening", ), "fontFamily": MessageLookupByLibrary.simpleMessage("FontFamily"), "fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"), "general": MessageLookupByLibrary.simpleMessage("General"), "generalDesc": MessageLookupByLibrary.simpleMessage( - "Overwrite general settings", + "Modify general settings", ), "geoData": MessageLookupByLibrary.simpleMessage("GeoData"), "geodataLoader": MessageLookupByLibrary.simpleMessage( @@ -259,6 +277,9 @@ class MessageLookup extends MessageLookupByLibrary { "Enabling will use the Geo low memory loader", ), "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip code"), + "getOriginRules": MessageLookupByLibrary.simpleMessage( + "Get original rules", + ), "global": MessageLookupByLibrary.simpleMessage("Global"), "go": MessageLookupByLibrary.simpleMessage("Go"), "goDownload": MessageLookupByLibrary.simpleMessage("Go to download"), @@ -302,6 +323,9 @@ class MessageLookup extends MessageLookupByLibrary { "Tcp keep alive interval", ), "key": MessageLookupByLibrary.simpleMessage("Key"), + "keyExists": MessageLookupByLibrary.simpleMessage( + "The current key already exists", + ), "language": MessageLookupByLibrary.simpleMessage("Language"), "layout": MessageLookupByLibrary.simpleMessage("Layout"), "light": MessageLookupByLibrary.simpleMessage("Light"), @@ -366,6 +390,8 @@ class MessageLookup extends MessageLookupByLibrary { "noProxyDesc": MessageLookupByLibrary.simpleMessage( "Please create a profile or add a valid profile", ), + "noResolve": MessageLookupByLibrary.simpleMessage("No resolve IP"), + "none": MessageLookupByLibrary.simpleMessage("none"), "notEmpty": MessageLookupByLibrary.simpleMessage("Cannot be empty"), "notSelectedTip": MessageLookupByLibrary.simpleMessage( "The current proxy group cannot be selected.", @@ -407,6 +433,9 @@ class MessageLookup extends MessageLookupByLibrary { "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( "Turning it on will override the DNS options in the profile", ), + "overrideOriginRules": MessageLookupByLibrary.simpleMessage( + "Override the original rule", + ), "password": MessageLookupByLibrary.simpleMessage("Password"), "passwordTip": MessageLookupByLibrary.simpleMessage( "Password cannot be empty", @@ -483,6 +512,7 @@ class MessageLookup extends MessageLookupByLibrary { "Only recovery profiles", ), "recoverySuccess": MessageLookupByLibrary.simpleMessage("Recovery success"), + "redo": MessageLookupByLibrary.simpleMessage("redo"), "regExp": MessageLookupByLibrary.simpleMessage("RegExp"), "remote": MessageLookupByLibrary.simpleMessage("Remote"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage( @@ -517,12 +547,27 @@ class MessageLookup extends MessageLookupByLibrary { "routeMode_config": MessageLookupByLibrary.simpleMessage("Use config"), "ru": MessageLookupByLibrary.simpleMessage("Russian"), "rule": MessageLookupByLibrary.simpleMessage("Rule"), + "ruleName": MessageLookupByLibrary.simpleMessage("Rule name"), + "ruleProviderEmptyTip": MessageLookupByLibrary.simpleMessage( + "Rule provider cannot be empty", + ), "ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"), + "ruleTarget": MessageLookupByLibrary.simpleMessage("Rule target"), + "ruleTargetEmptyTip": MessageLookupByLibrary.simpleMessage( + "Rule target cannot be empty", + ), "save": MessageLookupByLibrary.simpleMessage("Save"), + "saveChanges": MessageLookupByLibrary.simpleMessage( + "Do you want to save the changes?", + ), + "saveTip": MessageLookupByLibrary.simpleMessage( + "Are you sure you want to save?", + ), "search": MessageLookupByLibrary.simpleMessage("Search"), "seconds": MessageLookupByLibrary.simpleMessage("Seconds"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selected": MessageLookupByLibrary.simpleMessage("Selected"), + "selectedCountTitle": m0, "settings": MessageLookupByLibrary.simpleMessage("Settings"), "show": MessageLookupByLibrary.simpleMessage("Show"), "shrink": MessageLookupByLibrary.simpleMessage("Shrink"), @@ -533,6 +578,7 @@ class MessageLookup extends MessageLookupByLibrary { "size": MessageLookupByLibrary.simpleMessage("Size"), "sort": MessageLookupByLibrary.simpleMessage("Sort"), "source": MessageLookupByLibrary.simpleMessage("Source"), + "sourceIp": MessageLookupByLibrary.simpleMessage("Source IP"), "stackMode": MessageLookupByLibrary.simpleMessage("Stack mode"), "standard": MessageLookupByLibrary.simpleMessage("Standard"), "start": MessageLookupByLibrary.simpleMessage("Start"), @@ -544,6 +590,10 @@ class MessageLookup extends MessageLookupByLibrary { "stop": MessageLookupByLibrary.simpleMessage("Stop"), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "style": MessageLookupByLibrary.simpleMessage("Style"), + "subRule": MessageLookupByLibrary.simpleMessage("Sub rule"), + "subRuleEmptyTip": MessageLookupByLibrary.simpleMessage( + "Sub rule content cannot be empty", + ), "submit": MessageLookupByLibrary.simpleMessage("Submit"), "sync": MessageLookupByLibrary.simpleMessage("Sync"), "system": MessageLookupByLibrary.simpleMessage("System"), @@ -555,7 +605,7 @@ class MessageLookup extends MessageLookupByLibrary { "tab": MessageLookupByLibrary.simpleMessage("Tab"), "tabAnimation": MessageLookupByLibrary.simpleMessage("Tab animation"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( - "When enabled, the home tab will add a toggle animation", + "Effective only in mobile view", ), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP concurrent"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage( @@ -583,6 +633,7 @@ class MessageLookup extends MessageLookupByLibrary { "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "unable to update current profile", ), + "undo": MessageLookupByLibrary.simpleMessage("undo"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("Unified delay"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage( "Remove extra delays such as handshaking", @@ -597,6 +648,9 @@ class MessageLookup extends MessageLookupByLibrary { "useHosts": MessageLookupByLibrary.simpleMessage("Use hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("Use system hosts"), "value": MessageLookupByLibrary.simpleMessage("Value"), + "valueExists": MessageLookupByLibrary.simpleMessage( + "The current value already exists", + ), "view": MessageLookupByLibrary.simpleMessage("View"), "vpnDesc": MessageLookupByLibrary.simpleMessage( "Modify VPN related settings", diff --git a/lib/l10n/intl/messages_ja.dart b/lib/l10n/intl/messages_ja.dart index 38c7fc4..8aa6904 100644 --- a/lib/l10n/intl/messages_ja.dart +++ b/lib/l10n/intl/messages_ja.dart @@ -20,6 +20,8 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ja'; + static String m0(count) => "${count} 項目が選択されています"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("について"), @@ -42,6 +44,8 @@ class MessageLookup extends MessageLookupByLibrary { "action_tun": MessageLookupByLibrary.simpleMessage("TUN"), "action_view": MessageLookupByLibrary.simpleMessage("表示/非表示"), "add": MessageLookupByLibrary.simpleMessage("追加"), + "addRule": MessageLookupByLibrary.simpleMessage("ルールを追加"), + "addedOriginRules": MessageLookupByLibrary.simpleMessage("元のルールに追加"), "address": MessageLookupByLibrary.simpleMessage("アドレス"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAVサーバーアドレス"), "addressTip": MessageLookupByLibrary.simpleMessage("有効なWebDAVアドレスを入力"), @@ -82,6 +86,8 @@ class MessageLookup extends MessageLookupByLibrary { "WebDAVまたはファイルでデータを同期", ), "backupSuccess": MessageLookupByLibrary.simpleMessage("バックアップ成功"), + "basicConfig": MessageLookupByLibrary.simpleMessage("基本設定"), + "basicConfigDesc": MessageLookupByLibrary.simpleMessage("基本設定をグローバルに変更"), "bind": MessageLookupByLibrary.simpleMessage("バインド"), "blacklistMode": MessageLookupByLibrary.simpleMessage("ブラックリストモード"), "bypassDomain": MessageLookupByLibrary.simpleMessage("バイパスドメイン"), @@ -109,6 +115,8 @@ class MessageLookup extends MessageLookupByLibrary { "connections": MessageLookupByLibrary.simpleMessage("接続"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("現在の接続データを表示"), "connectivity": MessageLookupByLibrary.simpleMessage("接続性:"), + "content": MessageLookupByLibrary.simpleMessage("内容"), + "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容は必須です"), "copy": MessageLookupByLibrary.simpleMessage("コピー"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("環境変数をコピー"), "copyLink": MessageLookupByLibrary.simpleMessage("リンクをコピー"), @@ -133,6 +141,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteProfileTip": MessageLookupByLibrary.simpleMessage( "現在のプロファイルを削除しますか?", ), + "deleteRuleTip": MessageLookupByLibrary.simpleMessage("選択したルールを削除しますか?"), "desc": MessageLookupByLibrary.simpleMessage( "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。", ), @@ -151,6 +160,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("ダウンロード"), "edit": MessageLookupByLibrary.simpleMessage("編集"), "en": MessageLookupByLibrary.simpleMessage("英語"), + "enableOverride": MessageLookupByLibrary.simpleMessage("上書きを有効化"), "entries": MessageLookupByLibrary.simpleMessage(" エントリ"), "exclude": MessageLookupByLibrary.simpleMessage("最近のタスクから非表示"), "excludeDesc": MessageLookupByLibrary.simpleMessage( @@ -181,18 +191,19 @@ class MessageLookup extends MessageLookupByLibrary { "filterSystemApp": MessageLookupByLibrary.simpleMessage("システムアプリを除外"), "findProcessMode": MessageLookupByLibrary.simpleMessage("プロセス検出"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage( - "有効化するとフラッシュバックのリスクあり", + "有効化するとパフォーマンスが若干低下します", ), "fontFamily": MessageLookupByLibrary.simpleMessage("フォントファミリー"), "fourColumns": MessageLookupByLibrary.simpleMessage("4列"), "general": MessageLookupByLibrary.simpleMessage("一般"), - "generalDesc": MessageLookupByLibrary.simpleMessage("一般設定の上書き"), + "generalDesc": MessageLookupByLibrary.simpleMessage("一般設定を変更"), "geoData": MessageLookupByLibrary.simpleMessage("地域データ"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低メモリモード"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "有効化するとGeo低メモリローダーを使用", ), "geoipCode": MessageLookupByLibrary.simpleMessage("GeoIPコード"), + "getOriginRules": MessageLookupByLibrary.simpleMessage("元のルールを取得"), "global": MessageLookupByLibrary.simpleMessage("グローバル"), "go": MessageLookupByLibrary.simpleMessage("移動"), "goDownload": MessageLookupByLibrary.simpleMessage("ダウンロードへ"), @@ -222,6 +233,7 @@ class MessageLookup extends MessageLookupByLibrary { "TCPキープアライブ間隔", ), "key": MessageLookupByLibrary.simpleMessage("キー"), + "keyExists": MessageLookupByLibrary.simpleMessage("現在のキーは既に存在します"), "language": MessageLookupByLibrary.simpleMessage("言語"), "layout": MessageLookupByLibrary.simpleMessage("レイアウト"), "light": MessageLookupByLibrary.simpleMessage("ライト"), @@ -270,6 +282,8 @@ class MessageLookup extends MessageLookupByLibrary { "noProxyDesc": MessageLookupByLibrary.simpleMessage( "プロファイルを作成するか、有効なプロファイルを追加してください", ), + "noResolve": MessageLookupByLibrary.simpleMessage("IPを解決しない"), + "none": MessageLookupByLibrary.simpleMessage("なし"), "notEmpty": MessageLookupByLibrary.simpleMessage("空欄不可"), "notSelectedTip": MessageLookupByLibrary.simpleMessage( "現在のプロキシグループは選択できません", @@ -299,6 +313,7 @@ class MessageLookup extends MessageLookupByLibrary { "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( "有効化するとプロファイルのDNS設定を上書き", ), + "overrideOriginRules": MessageLookupByLibrary.simpleMessage("元のルールを上書き"), "password": MessageLookupByLibrary.simpleMessage("パスワード"), "passwordTip": MessageLookupByLibrary.simpleMessage("パスワードは必須です"), "paste": MessageLookupByLibrary.simpleMessage("貼り付け"), @@ -359,6 +374,7 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryAll": MessageLookupByLibrary.simpleMessage("全データ復元"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("プロファイルのみ復元"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("復元成功"), + "redo": MessageLookupByLibrary.simpleMessage("やり直す"), "regExp": MessageLookupByLibrary.simpleMessage("正規表現"), "remote": MessageLookupByLibrary.simpleMessage("リモート"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage( @@ -387,12 +403,21 @@ class MessageLookup extends MessageLookupByLibrary { "routeMode_config": MessageLookupByLibrary.simpleMessage("設定を使用"), "ru": MessageLookupByLibrary.simpleMessage("ロシア語"), "rule": MessageLookupByLibrary.simpleMessage("ルール"), + "ruleName": MessageLookupByLibrary.simpleMessage("ルール名"), + "ruleProviderEmptyTip": MessageLookupByLibrary.simpleMessage( + "ルールプロバイダーは必須です", + ), "ruleProviders": MessageLookupByLibrary.simpleMessage("ルールプロバイダー"), + "ruleTarget": MessageLookupByLibrary.simpleMessage("ルール対象"), + "ruleTargetEmptyTip": MessageLookupByLibrary.simpleMessage("ルール対象は必須です"), "save": MessageLookupByLibrary.simpleMessage("保存"), + "saveChanges": MessageLookupByLibrary.simpleMessage("変更を保存しますか?"), + "saveTip": MessageLookupByLibrary.simpleMessage("保存してもよろしいですか?"), "search": MessageLookupByLibrary.simpleMessage("検索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), "selectAll": MessageLookupByLibrary.simpleMessage("すべて選択"), "selected": MessageLookupByLibrary.simpleMessage("選択済み"), + "selectedCountTitle": m0, "settings": MessageLookupByLibrary.simpleMessage("設定"), "show": MessageLookupByLibrary.simpleMessage("表示"), "shrink": MessageLookupByLibrary.simpleMessage("縮小"), @@ -401,6 +426,7 @@ class MessageLookup extends MessageLookupByLibrary { "size": MessageLookupByLibrary.simpleMessage("サイズ"), "sort": MessageLookupByLibrary.simpleMessage("並び替え"), "source": MessageLookupByLibrary.simpleMessage("ソース"), + "sourceIp": MessageLookupByLibrary.simpleMessage("送信元IP"), "stackMode": MessageLookupByLibrary.simpleMessage("スタックモード"), "standard": MessageLookupByLibrary.simpleMessage("標準"), "start": MessageLookupByLibrary.simpleMessage("開始"), @@ -410,6 +436,8 @@ class MessageLookup extends MessageLookupByLibrary { "stop": MessageLookupByLibrary.simpleMessage("停止"), "stopVpn": MessageLookupByLibrary.simpleMessage("VPNを停止中..."), "style": MessageLookupByLibrary.simpleMessage("スタイル"), + "subRule": MessageLookupByLibrary.simpleMessage("サブルール"), + "subRuleEmptyTip": MessageLookupByLibrary.simpleMessage("サブルールの内容は必須です"), "submit": MessageLookupByLibrary.simpleMessage("送信"), "sync": MessageLookupByLibrary.simpleMessage("同期"), "system": MessageLookupByLibrary.simpleMessage("システム"), @@ -420,9 +448,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "tab": MessageLookupByLibrary.simpleMessage("タブ"), "tabAnimation": MessageLookupByLibrary.simpleMessage("タブアニメーション"), - "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( - "有効化するとホームタブに切り替えアニメーションを追加", - ), + "tabAnimationDesc": MessageLookupByLibrary.simpleMessage("モバイル表示でのみ有効"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"), "testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"), @@ -443,6 +469,7 @@ class MessageLookup extends MessageLookupByLibrary { "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "現在のプロファイルを更新できません", ), + "undo": MessageLookupByLibrary.simpleMessage("元に戻す"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("統一遅延"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage( "ハンドシェイクなどの余分な遅延を削除", @@ -455,6 +482,7 @@ class MessageLookup extends MessageLookupByLibrary { "useHosts": MessageLookupByLibrary.simpleMessage("ホストを使用"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("システムホストを使用"), "value": MessageLookupByLibrary.simpleMessage("値"), + "valueExists": MessageLookupByLibrary.simpleMessage("現在の値は既に存在します"), "view": MessageLookupByLibrary.simpleMessage("表示"), "vpnDesc": MessageLookupByLibrary.simpleMessage("VPN関連設定の変更"), "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( diff --git a/lib/l10n/intl/messages_ru.dart b/lib/l10n/intl/messages_ru.dart index 5a17d5f..1a0e7eb 100644 --- a/lib/l10n/intl/messages_ru.dart +++ b/lib/l10n/intl/messages_ru.dart @@ -20,6 +20,8 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ru'; + static String m0(count) => "Выбрано ${count} элементов"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("О программе"), @@ -44,6 +46,10 @@ class MessageLookup extends MessageLookupByLibrary { "action_tun": MessageLookupByLibrary.simpleMessage("TUN"), "action_view": MessageLookupByLibrary.simpleMessage("Показать/Скрыть"), "add": MessageLookupByLibrary.simpleMessage("Добавить"), + "addRule": MessageLookupByLibrary.simpleMessage("Добавить правило"), + "addedOriginRules": MessageLookupByLibrary.simpleMessage( + "Добавить к оригинальным правилам", + ), "address": MessageLookupByLibrary.simpleMessage("Адрес"), "addressHelp": MessageLookupByLibrary.simpleMessage("Адрес сервера WebDAV"), "addressTip": MessageLookupByLibrary.simpleMessage( @@ -114,6 +120,10 @@ class MessageLookup extends MessageLookupByLibrary { "backupSuccess": MessageLookupByLibrary.simpleMessage( "Резервное копирование успешно", ), + "basicConfig": MessageLookupByLibrary.simpleMessage("Базовая конфигурация"), + "basicConfigDesc": MessageLookupByLibrary.simpleMessage( + "Глобальное изменение базовых настроек", + ), "bind": MessageLookupByLibrary.simpleMessage("Привязать"), "blacklistMode": MessageLookupByLibrary.simpleMessage( "Режим черного списка", @@ -155,6 +165,10 @@ class MessageLookup extends MessageLookupByLibrary { "Просмотр текущих данных о соединениях", ), "connectivity": MessageLookupByLibrary.simpleMessage("Связь:"), + "content": MessageLookupByLibrary.simpleMessage("Содержание"), + "contentEmptyTip": MessageLookupByLibrary.simpleMessage( + "Содержание не может быть пустым", + ), "copy": MessageLookupByLibrary.simpleMessage("Копировать"), "copyEnvVar": MessageLookupByLibrary.simpleMessage( "Копирование переменных окружения", @@ -185,6 +199,9 @@ class MessageLookup extends MessageLookupByLibrary { "deleteProfileTip": MessageLookupByLibrary.simpleMessage( "Вы уверены, что хотите удалить текущий профиль?", ), + "deleteRuleTip": MessageLookupByLibrary.simpleMessage( + "Вы уверены, что хотите удалить выбранное правило?", + ), "desc": MessageLookupByLibrary.simpleMessage( "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.", ), @@ -215,6 +232,9 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("Скачивание"), "edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "en": MessageLookupByLibrary.simpleMessage("Английский"), + "enableOverride": MessageLookupByLibrary.simpleMessage( + "Включить переопределение", + ), "entries": MessageLookupByLibrary.simpleMessage(" записей"), "exclude": MessageLookupByLibrary.simpleMessage( "Скрыть из последних задач", @@ -259,13 +279,13 @@ class MessageLookup extends MessageLookupByLibrary { "Режим поиска процесса", ), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage( - "Есть риск сбоя после включения", + "При включении возможны небольшие потери производительности", ), "fontFamily": MessageLookupByLibrary.simpleMessage("Семейство шрифтов"), "fourColumns": MessageLookupByLibrary.simpleMessage("Четыре столбца"), "general": MessageLookupByLibrary.simpleMessage("Общие"), "generalDesc": MessageLookupByLibrary.simpleMessage( - "Переопределение общих настроек", + "Изменение общих настроек", ), "geoData": MessageLookupByLibrary.simpleMessage("Геоданные"), "geodataLoader": MessageLookupByLibrary.simpleMessage( @@ -275,6 +295,9 @@ class MessageLookup extends MessageLookupByLibrary { "Включение будет использовать загрузчик геоданных с низким потреблением памяти", ), "geoipCode": MessageLookupByLibrary.simpleMessage("Код Geoip"), + "getOriginRules": MessageLookupByLibrary.simpleMessage( + "Получить оригинальные правила", + ), "global": MessageLookupByLibrary.simpleMessage("Глобальный"), "go": MessageLookupByLibrary.simpleMessage("Перейти"), "goDownload": MessageLookupByLibrary.simpleMessage("Перейти к загрузке"), @@ -322,6 +345,9 @@ class MessageLookup extends MessageLookupByLibrary { "Интервал поддержания TCP-соединения", ), "key": MessageLookupByLibrary.simpleMessage("Ключ"), + "keyExists": MessageLookupByLibrary.simpleMessage( + "Текущий ключ уже существует", + ), "language": MessageLookupByLibrary.simpleMessage("Язык"), "layout": MessageLookupByLibrary.simpleMessage("Макет"), "light": MessageLookupByLibrary.simpleMessage("Светлый"), @@ -392,6 +418,8 @@ class MessageLookup extends MessageLookupByLibrary { "noProxyDesc": MessageLookupByLibrary.simpleMessage( "Пожалуйста, создайте профиль или добавьте действительный профиль", ), + "noResolve": MessageLookupByLibrary.simpleMessage("Не разрешать IP"), + "none": MessageLookupByLibrary.simpleMessage("Нет"), "notEmpty": MessageLookupByLibrary.simpleMessage("Не может быть пустым"), "notSelectedTip": MessageLookupByLibrary.simpleMessage( "Текущая группа прокси не может быть выбрана.", @@ -435,6 +463,9 @@ class MessageLookup extends MessageLookupByLibrary { "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( "Включение переопределит настройки DNS в профиле", ), + "overrideOriginRules": MessageLookupByLibrary.simpleMessage( + "Переопределить оригинальное правило", + ), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "passwordTip": MessageLookupByLibrary.simpleMessage( "Пароль не может быть пустым", @@ -517,6 +548,7 @@ class MessageLookup extends MessageLookupByLibrary { "recoverySuccess": MessageLookupByLibrary.simpleMessage( "Восстановление успешно", ), + "redo": MessageLookupByLibrary.simpleMessage("Повторить"), "regExp": MessageLookupByLibrary.simpleMessage("Регулярное выражение"), "remote": MessageLookupByLibrary.simpleMessage("Удаленный"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage( @@ -555,12 +587,25 @@ class MessageLookup extends MessageLookupByLibrary { ), "ru": MessageLookupByLibrary.simpleMessage("Русский"), "rule": MessageLookupByLibrary.simpleMessage("Правило"), + "ruleName": MessageLookupByLibrary.simpleMessage("Название правила"), + "ruleProviderEmptyTip": MessageLookupByLibrary.simpleMessage( + "Поставщик правил не может быть пустым", + ), "ruleProviders": MessageLookupByLibrary.simpleMessage("Провайдеры правил"), + "ruleTarget": MessageLookupByLibrary.simpleMessage("Цель правила"), + "ruleTargetEmptyTip": MessageLookupByLibrary.simpleMessage( + "Цель правила не может быть пустой", + ), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), + "saveChanges": MessageLookupByLibrary.simpleMessage("Сохранить изменения?"), + "saveTip": MessageLookupByLibrary.simpleMessage( + "Вы уверены, что хотите сохранить?", + ), "search": MessageLookupByLibrary.simpleMessage("Поиск"), "seconds": MessageLookupByLibrary.simpleMessage("Секунд"), "selectAll": MessageLookupByLibrary.simpleMessage("Выбрать все"), "selected": MessageLookupByLibrary.simpleMessage("Выбрано"), + "selectedCountTitle": m0, "settings": MessageLookupByLibrary.simpleMessage("Настройки"), "show": MessageLookupByLibrary.simpleMessage("Показать"), "shrink": MessageLookupByLibrary.simpleMessage("Сжать"), @@ -571,6 +616,7 @@ class MessageLookup extends MessageLookupByLibrary { "size": MessageLookupByLibrary.simpleMessage("Размер"), "sort": MessageLookupByLibrary.simpleMessage("Сортировка"), "source": MessageLookupByLibrary.simpleMessage("Источник"), + "sourceIp": MessageLookupByLibrary.simpleMessage("Исходный IP"), "stackMode": MessageLookupByLibrary.simpleMessage("Режим стека"), "standard": MessageLookupByLibrary.simpleMessage("Стандартный"), "start": MessageLookupByLibrary.simpleMessage("Старт"), @@ -582,6 +628,10 @@ class MessageLookup extends MessageLookupByLibrary { "stop": MessageLookupByLibrary.simpleMessage("Стоп"), "stopVpn": MessageLookupByLibrary.simpleMessage("Остановка VPN..."), "style": MessageLookupByLibrary.simpleMessage("Стиль"), + "subRule": MessageLookupByLibrary.simpleMessage("Подправило"), + "subRuleEmptyTip": MessageLookupByLibrary.simpleMessage( + "Содержание подправила не может быть пустым", + ), "submit": MessageLookupByLibrary.simpleMessage("Отправить"), "sync": MessageLookupByLibrary.simpleMessage("Синхронизация"), "system": MessageLookupByLibrary.simpleMessage("Система"), @@ -593,7 +643,7 @@ class MessageLookup extends MessageLookupByLibrary { "tab": MessageLookupByLibrary.simpleMessage("Вкладка"), "tabAnimation": MessageLookupByLibrary.simpleMessage("Анимация вкладок"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( - "При включении домашняя вкладка добавит анимацию переключения", + "Действительно только в мобильном виде", ), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP параллелизм"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage( @@ -623,6 +673,7 @@ class MessageLookup extends MessageLookupByLibrary { "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "невозможно обновить текущий профиль", ), + "undo": MessageLookupByLibrary.simpleMessage("Отменить"), "unifiedDelay": MessageLookupByLibrary.simpleMessage( "Унифицированная задержка", ), @@ -641,6 +692,9 @@ class MessageLookup extends MessageLookupByLibrary { "Использовать системные hosts", ), "value": MessageLookupByLibrary.simpleMessage("Значение"), + "valueExists": MessageLookupByLibrary.simpleMessage( + "Текущее значение уже существует", + ), "view": MessageLookupByLibrary.simpleMessage("Просмотр"), "vpnDesc": MessageLookupByLibrary.simpleMessage( "Изменение настроек, связанных с VPN", diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index b3adcad..5135f85 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -20,6 +20,8 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh_CN'; + static String m0(count) => "已选择 ${count} 项"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("关于"), @@ -40,6 +42,8 @@ class MessageLookup extends MessageLookupByLibrary { "action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"), "add": MessageLookupByLibrary.simpleMessage("添加"), + "addRule": MessageLookupByLibrary.simpleMessage("添加规则"), + "addedOriginRules": MessageLookupByLibrary.simpleMessage("附加到原始规则"), "address": MessageLookupByLibrary.simpleMessage("地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), @@ -76,6 +80,8 @@ class MessageLookup extends MessageLookupByLibrary { "通过WebDAV或者文件同步数据", ), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), + "basicConfig": MessageLookupByLibrary.simpleMessage("基本配置"), + "basicConfigDesc": MessageLookupByLibrary.simpleMessage("全局修改基本配置"), "bind": MessageLookupByLibrary.simpleMessage("绑定"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), "bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"), @@ -99,6 +105,8 @@ class MessageLookup extends MessageLookupByLibrary { "connections": MessageLookupByLibrary.simpleMessage("连接"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), + "content": MessageLookupByLibrary.simpleMessage("内容"), + "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容不能为空"), "copy": MessageLookupByLibrary.simpleMessage("复制"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"), "copyLink": MessageLookupByLibrary.simpleMessage("复制链接"), @@ -119,6 +127,7 @@ class MessageLookup extends MessageLookupByLibrary { "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delete": MessageLookupByLibrary.simpleMessage("删除"), "deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"), + "deleteRuleTip": MessageLookupByLibrary.simpleMessage("确定要删除选中的规则吗?"), "desc": MessageLookupByLibrary.simpleMessage( "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。", ), @@ -137,6 +146,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("下载"), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "en": MessageLookupByLibrary.simpleMessage("英语"), + "enableOverride": MessageLookupByLibrary.simpleMessage("启用覆写"), "entries": MessageLookupByLibrary.simpleMessage("个条目"), "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), @@ -162,15 +172,16 @@ class MessageLookup extends MessageLookupByLibrary { "fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), "findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"), - "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"), + "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后会有一定性能损耗"), "fontFamily": MessageLookupByLibrary.simpleMessage("字体"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"), - "general": MessageLookupByLibrary.simpleMessage("基础"), - "generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"), + "general": MessageLookupByLibrary.simpleMessage("常规"), + "generalDesc": MessageLookupByLibrary.simpleMessage("修改通用设置"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"), "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"), + "getOriginRules": MessageLookupByLibrary.simpleMessage("获取原始规则"), "global": MessageLookupByLibrary.simpleMessage("全局"), "go": MessageLookupByLibrary.simpleMessage("前往"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), @@ -196,6 +207,7 @@ class MessageLookup extends MessageLookupByLibrary { "just": MessageLookupByLibrary.simpleMessage("刚刚"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "key": MessageLookupByLibrary.simpleMessage("键"), + "keyExists": MessageLookupByLibrary.simpleMessage("当前键已存在"), "language": MessageLookupByLibrary.simpleMessage("语言"), "layout": MessageLookupByLibrary.simpleMessage("布局"), "light": MessageLookupByLibrary.simpleMessage("浅色"), @@ -238,6 +250,8 @@ class MessageLookup extends MessageLookupByLibrary { "noNetwork": MessageLookupByLibrary.simpleMessage("无网络"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), + "noResolve": MessageLookupByLibrary.simpleMessage("不解析IP"), + "none": MessageLookupByLibrary.simpleMessage("无"), "notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"), "nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"), @@ -261,6 +275,7 @@ class MessageLookup extends MessageLookupByLibrary { "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), "overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"), + "overrideOriginRules": MessageLookupByLibrary.simpleMessage("覆盖原始规则"), "password": MessageLookupByLibrary.simpleMessage("密码"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"), @@ -313,6 +328,7 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), + "redo": MessageLookupByLibrary.simpleMessage("重做"), "regExp": MessageLookupByLibrary.simpleMessage("正则"), "remote": MessageLookupByLibrary.simpleMessage("远程"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), @@ -335,12 +351,19 @@ class MessageLookup extends MessageLookupByLibrary { "routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"), "ru": MessageLookupByLibrary.simpleMessage("俄语"), "rule": MessageLookupByLibrary.simpleMessage("规则"), + "ruleName": MessageLookupByLibrary.simpleMessage("规则名称"), + "ruleProviderEmptyTip": MessageLookupByLibrary.simpleMessage("规则提供者不能为空"), "ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"), + "ruleTarget": MessageLookupByLibrary.simpleMessage("规则目标"), + "ruleTargetEmptyTip": MessageLookupByLibrary.simpleMessage("规则目标不能为空"), "save": MessageLookupByLibrary.simpleMessage("保存"), + "saveChanges": MessageLookupByLibrary.simpleMessage("是否保存更改?"), + "saveTip": MessageLookupByLibrary.simpleMessage("确定要保存吗?"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selected": MessageLookupByLibrary.simpleMessage("已选择"), + "selectedCountTitle": m0, "settings": MessageLookupByLibrary.simpleMessage("设置"), "show": MessageLookupByLibrary.simpleMessage("显示"), "shrink": MessageLookupByLibrary.simpleMessage("紧凑"), @@ -349,6 +372,7 @@ class MessageLookup extends MessageLookupByLibrary { "size": MessageLookupByLibrary.simpleMessage("尺寸"), "sort": MessageLookupByLibrary.simpleMessage("排序"), "source": MessageLookupByLibrary.simpleMessage("来源"), + "sourceIp": MessageLookupByLibrary.simpleMessage("源IP"), "stackMode": MessageLookupByLibrary.simpleMessage("栈模式"), "standard": MessageLookupByLibrary.simpleMessage("标准"), "start": MessageLookupByLibrary.simpleMessage("启动"), @@ -358,6 +382,8 @@ class MessageLookup extends MessageLookupByLibrary { "stop": MessageLookupByLibrary.simpleMessage("暂停"), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "style": MessageLookupByLibrary.simpleMessage("风格"), + "subRule": MessageLookupByLibrary.simpleMessage("子规则"), + "subRuleEmptyTip": MessageLookupByLibrary.simpleMessage("子规则内容不能为空"), "submit": MessageLookupByLibrary.simpleMessage("提交"), "sync": MessageLookupByLibrary.simpleMessage("同步"), "system": MessageLookupByLibrary.simpleMessage("系统"), @@ -366,9 +392,7 @@ class MessageLookup extends MessageLookupByLibrary { "systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"), "tab": MessageLookupByLibrary.simpleMessage("标签页"), "tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"), - "tabAnimationDesc": MessageLookupByLibrary.simpleMessage( - "开启后,主页选项卡将添加切换动画", - ), + "tabAnimationDesc": MessageLookupByLibrary.simpleMessage("仅在移动视图中有效"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "testUrl": MessageLookupByLibrary.simpleMessage("测速链接"), @@ -389,6 +413,7 @@ class MessageLookup extends MessageLookupByLibrary { "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage( "无法更新当前配置文件", ), + "undo": MessageLookupByLibrary.simpleMessage("撤销"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"), "unknown": MessageLookupByLibrary.simpleMessage("未知"), @@ -399,6 +424,7 @@ class MessageLookup extends MessageLookupByLibrary { "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "value": MessageLookupByLibrary.simpleMessage("值"), + "valueExists": MessageLookupByLibrary.simpleMessage("当前值已存在"), "view": MessageLookupByLibrary.simpleMessage("查看"), "vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"), "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 214e2fa..a9f505e 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -945,16 +945,6 @@ class AppLocalizations { ); } - /// `When enabled, the home tab will add a toggle animation` - String get tabAnimationDesc { - return Intl.message( - 'When enabled, the home tab will add a toggle animation', - name: 'tabAnimationDesc', - desc: '', - args: [], - ); - } - /// `A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.` String get desc { return Intl.message( @@ -1435,16 +1425,6 @@ class AppLocalizations { ); } - /// `There is a risk of flashback after opening` - String get findProcessModeDesc { - return Intl.message( - 'There is a risk of flashback after opening', - name: 'findProcessModeDesc', - desc: '', - args: [], - ); - } - /// `Init` String get init { return Intl.message('Init', name: 'init', desc: '', args: []); @@ -1930,16 +1910,6 @@ class AppLocalizations { ); } - /// `Overwrite general settings` - String get generalDesc { - return Intl.message( - 'Overwrite general settings', - name: 'generalDesc', - desc: '', - args: [], - ); - } - /// `Update DNS related settings` String get dnsDesc { return Intl.message( @@ -2694,6 +2664,246 @@ class AppLocalizations { String get listen { return Intl.message('Listen', name: 'listen', desc: '', args: []); } + + /// `The current key already exists` + String get keyExists { + return Intl.message( + 'The current key already exists', + name: 'keyExists', + desc: '', + args: [], + ); + } + + /// `The current value already exists` + String get valueExists { + return Intl.message( + 'The current value already exists', + name: 'valueExists', + desc: '', + args: [], + ); + } + + /// `undo` + String get undo { + return Intl.message('undo', name: 'undo', desc: '', args: []); + } + + /// `redo` + String get redo { + return Intl.message('redo', name: 'redo', desc: '', args: []); + } + + /// `none` + String get none { + return Intl.message('none', name: 'none', desc: '', args: []); + } + + /// `Basic configuration` + String get basicConfig { + return Intl.message( + 'Basic configuration', + name: 'basicConfig', + desc: '', + args: [], + ); + } + + /// `Modify the basic configuration globally` + String get basicConfigDesc { + return Intl.message( + 'Modify the basic configuration globally', + name: 'basicConfigDesc', + desc: '', + args: [], + ); + } + + /// `{count} items have been selected` + String selectedCountTitle(Object count) { + return Intl.message( + '$count items have been selected', + name: 'selectedCountTitle', + desc: '', + args: [count], + ); + } + + /// `Add rule` + String get addRule { + return Intl.message('Add rule', name: 'addRule', desc: '', args: []); + } + + /// `Rule provider cannot be empty` + String get ruleProviderEmptyTip { + return Intl.message( + 'Rule provider cannot be empty', + name: 'ruleProviderEmptyTip', + desc: '', + args: [], + ); + } + + /// `Rule name` + String get ruleName { + return Intl.message('Rule name', name: 'ruleName', desc: '', args: []); + } + + /// `Content` + String get content { + return Intl.message('Content', name: 'content', desc: '', args: []); + } + + /// `Content cannot be empty` + String get contentEmptyTip { + return Intl.message( + 'Content cannot be empty', + name: 'contentEmptyTip', + desc: '', + args: [], + ); + } + + /// `Sub rule` + String get subRule { + return Intl.message('Sub rule', name: 'subRule', desc: '', args: []); + } + + /// `Sub rule content cannot be empty` + String get subRuleEmptyTip { + return Intl.message( + 'Sub rule content cannot be empty', + name: 'subRuleEmptyTip', + desc: '', + args: [], + ); + } + + /// `Rule target` + String get ruleTarget { + return Intl.message('Rule target', name: 'ruleTarget', desc: '', args: []); + } + + /// `Rule target cannot be empty` + String get ruleTargetEmptyTip { + return Intl.message( + 'Rule target cannot be empty', + name: 'ruleTargetEmptyTip', + desc: '', + args: [], + ); + } + + /// `Source IP` + String get sourceIp { + return Intl.message('Source IP', name: 'sourceIp', desc: '', args: []); + } + + /// `No resolve IP` + String get noResolve { + return Intl.message('No resolve IP', name: 'noResolve', desc: '', args: []); + } + + /// `Get original rules` + String get getOriginRules { + return Intl.message( + 'Get original rules', + name: 'getOriginRules', + desc: '', + args: [], + ); + } + + /// `Override the original rule` + String get overrideOriginRules { + return Intl.message( + 'Override the original rule', + name: 'overrideOriginRules', + desc: '', + args: [], + ); + } + + /// `Attach on the original rules` + String get addedOriginRules { + return Intl.message( + 'Attach on the original rules', + name: 'addedOriginRules', + desc: '', + args: [], + ); + } + + /// `Enable override` + String get enableOverride { + return Intl.message( + 'Enable override', + name: 'enableOverride', + desc: '', + args: [], + ); + } + + /// `Are you sure you want to delete the selected rule?` + String get deleteRuleTip { + return Intl.message( + 'Are you sure you want to delete the selected rule?', + name: 'deleteRuleTip', + desc: '', + args: [], + ); + } + + /// `Do you want to save the changes?` + String get saveChanges { + return Intl.message( + 'Do you want to save the changes?', + name: 'saveChanges', + desc: '', + args: [], + ); + } + + /// `Modify general settings` + String get generalDesc { + return Intl.message( + 'Modify general settings', + name: 'generalDesc', + desc: '', + args: [], + ); + } + + /// `There is a certain performance loss after opening` + String get findProcessModeDesc { + return Intl.message( + 'There is a certain performance loss after opening', + name: 'findProcessModeDesc', + desc: '', + args: [], + ); + } + + /// `Effective only in mobile view` + String get tabAnimationDesc { + return Intl.message( + 'Effective only in mobile view', + name: 'tabAnimationDesc', + desc: '', + args: [], + ); + } + + /// `Are you sure you want to save?` + String get saveTip { + return Intl.message( + 'Are you sure you want to save?', + name: 'saveTip', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 3f95d19..91e9edf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,6 +27,7 @@ Future main() async { await globalState.initApp(version); await android?.init(); await window?.init(version); + globalState.isPre = const String.fromEnvironment("APP_ENV") != 'stable'; 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 abb64ee..5724b69 100644 --- a/lib/manager/app_state_manager.dart +++ b/lib/manager/app_state_manager.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -19,6 +21,7 @@ class _AppStateManagerState extends State @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); } @@ -56,3 +59,24 @@ class _AppStateManagerState extends State ); } } + +class AppEnvManager extends StatelessWidget { + final Widget child; + + const AppEnvManager({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + if (globalState.isPre) { + return Banner( + message: 'PRE', + location: BannerLocation.topEnd, + child: child, + ); + } + return child; + } +} diff --git a/lib/manager/manager.dart b/lib/manager/manager.dart index 3d435b6..e8208ca 100644 --- a/lib/manager/manager.dart +++ b/lib/manager/manager.dart @@ -7,4 +7,5 @@ export 'app_state_manager.dart'; export 'vpn_manager.dart'; export 'proxy_manager.dart'; export 'connectivity_manager.dart'; -export 'message_manager.dart'; \ No newline at end of file +export 'message_manager.dart'; +export 'theme_manager.dart'; \ No newline at end of file diff --git a/lib/manager/message_manager.dart b/lib/manager/message_manager.dart index 79842cd..11975b7 100644 --- a/lib/manager/message_manager.dart +++ b/lib/manager/message_manager.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'dart:math'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/fade_box.dart'; import 'package:flutter/material.dart'; class MessageManager extends StatefulWidget { @@ -17,41 +18,49 @@ class MessageManager extends StatefulWidget { State createState() => MessageManagerState(); } -class MessageManagerState extends State - with SingleTickerProviderStateMixin { +class MessageManagerState extends State { final _messagesNotifier = ValueNotifier>([]); - double maxWidth = 0; - Offset offset = Offset.zero; - - late AnimationController _animationController; - - final animationDuration = commonDuration * 2; + final List _bufferMessages = []; + Completer? _messageIngCompleter; @override void initState() { super.initState(); - _animationController = AnimationController( - vsync: this, - duration: Duration(milliseconds: 400), - ); } @override void dispose() { _messagesNotifier.dispose(); - _animationController.dispose(); super.dispose(); } - message(String text) async { + Future message(String text) async { final commonMessage = CommonMessage( id: other.uuidV4, text: text, ); - _messagesNotifier.value = List.from(_messagesNotifier.value) - ..add( - commonMessage, - ); + _bufferMessages.add(commonMessage); + _showMessage(); + } + + _showMessage() async { + if (_messageIngCompleter?.isCompleted == false) { + return; + } + 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); + } } _handleRemove(CommonMessage commonMessage) async { @@ -64,171 +73,49 @@ class MessageManagerState extends State return Stack( children: [ widget.child, - LayoutBuilder( - builder: (context, container) { - maxWidth = container.maxWidth / 2 + 16; - return SizedBox( - width: maxWidth, - child: ValueListenableBuilder( - valueListenable: globalState.safeMessageOffsetNotifier, - builder: (_, offset, child) { - this.offset = offset; - if (offset == Offset.zero) { - return SizedBox(); - } - return Transform.translate( - offset: offset, - child: child!, - ); - }, - child: Container( - padding: EdgeInsets.only( - right: 0, - left: 8, - top: 0, - bottom: 16, - ), - alignment: Alignment.bottomLeft, - child: Stack( - alignment: Alignment.bottomLeft, - children: [ - SingleChildScrollView( - reverse: true, - physics: NeverScrollableScrollPhysics(), - child: ValueListenableBuilder( - valueListenable: _messagesNotifier, - builder: (_, messages, ___) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (final message in messages) ...[ - if (message != messages.first) - SizedBox( - height: 12, - ), - _MessageItem( - key: GlobalObjectKey(message.id), - message: message, - onRemove: _handleRemove, - ), - ], - ], - ); - }, - ), - ), - ], - ), - ), - ), + ValueListenableBuilder( + valueListenable: _messagesNotifier, + builder: (_, messages, __) { + return FadeThroughBox( + 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, + 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, + ), + ), + ); + }, + ), ); }, - ) + ), ], ); } } - -class _MessageItem extends StatefulWidget { - final CommonMessage message; - final Function(CommonMessage message) onRemove; - - const _MessageItem({ - super.key, - required this.message, - required this.onRemove, - }); - - @override - State<_MessageItem> createState() => _MessageItemState(); -} - -class _MessageItemState extends State<_MessageItem> - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _offsetAnimation; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: commonDuration * 1.5, - ); - _offsetAnimation = Tween( - begin: Offset(-1.0, 0.0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _controller, - curve: Interval( - 0.0, - 1, - curve: Curves.easeOut, - ), - )); - - _fadeAnimation = Tween( - begin: 0.0, - end: 1, - ).animate(CurvedAnimation( - parent: _controller, - curve: Interval( - 0.0, - 0.2, - curve: Curves.easeIn, - ), - )); - - _controller.forward(); - - Future.delayed( - widget.message.duration, - () async { - await _controller.reverse(); - widget.onRemove( - widget.message, - ); - }, - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller.view, - builder: (_, child) { - return FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _offsetAnimation, - child: Material( - elevation: _controller.value * 12, - borderRadius: BorderRadius.circular(8), - color: context.colorScheme.surfaceContainer, - clipBehavior: Clip.none, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), - child: Text( - widget.message.text, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceVariant, - ), - maxLines: 5, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - ); - }, - ); - } -} diff --git a/lib/manager/theme_manager.dart b/lib/manager/theme_manager.dart new file mode 100644 index 0000000..feffea5 --- /dev/null +++ b/lib/manager/theme_manager.dart @@ -0,0 +1,38 @@ +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/state.dart'; +import 'package:flutter/material.dart'; + +class ThemeManager extends StatelessWidget { + final Widget child; + + const ThemeManager({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + globalState.measure = Measure.of(context); + globalState.theme = CommonTheme.of(context); + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + textScaleFactor, + ), + ), + child: LayoutBuilder( + builder: (_, container) { + globalState.appController.updateViewSize( + Size( + container.maxWidth, + container.maxHeight, + ), + ); + return child; + }, + ), + ); + } +} diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index 1aebae0..feff26a 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:fl_clash/common/common.dart'; @@ -24,6 +25,7 @@ class WindowManager extends ConsumerStatefulWidget { class _WindowContainerState extends ConsumerState with WindowListener, WindowExtListener { + @override Widget build(BuildContext context) { return widget.child; @@ -271,7 +273,7 @@ class _WindowHeaderState extends State { _updateMaximized(); }, child: Container( - color: context.colorScheme.secondary.toSoft, + color: context.colorScheme.secondary.opacity15, alignment: Alignment.centerLeft, height: kHeaderHeight, ), diff --git a/lib/models/app.dart b/lib/models/app.dart index 94228c7..d7f10b6 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -18,7 +18,7 @@ class AppState with _$AppState { @Default([]) List packages, @Default(ColorSchemes()) ColorSchemes colorSchemes, @Default(0) int sortNum, - required double viewWidth, + required Size viewSize, @Default({}) DelayMap delayMap, @Default([]) List groups, @Default(0) int checkIpNum, @@ -35,7 +35,7 @@ class AppState with _$AppState { } extension AppStateExt on AppState { - ViewMode get viewMode => other.getViewMode(viewWidth); + ViewMode get viewMode => other.getViewMode(viewSize.width); bool get isStart => runTime != null; } diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index c34795e..3c84864 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -6,6 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../enum/enum.dart'; part 'generated/clash_config.freezed.dart'; + part 'generated/clash_config.g.dart'; typedef HostsMap = Map; @@ -122,7 +123,7 @@ class ProxyGroup with _$ProxyGroup { String? filter, @JsonKey(name: "expected-filter") String? excludeFilter, @JsonKey(name: "exclude-type") String? excludeType, - @JsonKey(name: "expected-status") int? expectedStatus, + @JsonKey(name: "expected-status") dynamic expectedStatus, bool? hidden, String? icon, }) = _ProxyGroup; @@ -131,6 +132,16 @@ class ProxyGroup with _$ProxyGroup { _$ProxyGroupFromJson(json); } +@freezed +class RuleProvider with _$RuleProvider { + const factory RuleProvider({ + required String name, + }) = _RuleProvider; + + factory RuleProvider.fromJson(Map json) => + _$RuleProviderFromJson(json); +} + @freezed class Tun with _$Tun { const factory Tun({ @@ -273,11 +284,136 @@ class GeoXUrl with _$GeoXUrl { } } +@freezed +class ParsedRule with _$ParsedRule { + const factory ParsedRule({ + required RuleAction ruleAction, + String? content, + String? ruleTarget, + String? ruleProvider, + String? subRule, + @Default(false) bool noResolve, + @Default(false) bool src, + }) = _ParsedRule; + + factory ParsedRule.parseString(String value) { + final splits = value.split(","); + final shortSplits = splits + .where( + (item) => !item.contains("src") && !item.contains("no-resolve"), + ) + .toList(); + final ruleAction = RuleAction.values.firstWhere( + (item) => item.value == shortSplits.first, + orElse: () => RuleAction.DOMAIN, + ); + String? subRule; + String? ruleTarget; + + if (ruleAction == RuleAction.SUB_RULE) { + subRule = shortSplits.last; + } else { + ruleTarget = shortSplits.last; + } + + String? content; + String? ruleProvider; + + if (ruleAction == RuleAction.RULE_SET) { + ruleProvider = shortSplits.sublist(1, shortSplits.length - 1).join(","); + } else { + content = shortSplits.sublist(1, shortSplits.length - 1).join(","); + } + + return ParsedRule( + ruleAction: ruleAction, + content: content, + src: splits.contains("src"), + ruleProvider: ruleProvider, + noResolve: splits.contains("no-resolve"), + subRule: subRule, + ruleTarget: ruleTarget, + ); + } +} + +extension ParsedRuleExt on ParsedRule { + String get value { + return [ + ruleAction.value, + ruleAction == RuleAction.RULE_SET ? ruleProvider : content, + ruleAction == RuleAction.SUB_RULE ? subRule : ruleTarget, + if (ruleAction.hasParams) ...[ + if (src) "src", + if (noResolve) "no-resolve", + ] + ].join(","); + } +} + +@freezed +class Rule with _$Rule { + const factory Rule({ + required String id, + required String value, + }) = _Rule; + + factory Rule.value(String value) { + return Rule( + value: value, + id: other.uuidV4, + ); + } + + factory Rule.fromJson(Map json) => _$RuleFromJson(json); +} + +@freezed +class SubRule with _$SubRule { + const factory SubRule({ + required String name, + }) = _SubRule; + + factory SubRule.fromJson(Map json) => + _$SubRuleFromJson(json); +} + +_genRule(List? rules) { + if (rules == null) { + return []; + } + return rules + .map( + (item) => Rule.value(item), + ) + .toList(); +} + +List _genRuleProviders(Map json) { + return json.entries.map((entry) => RuleProvider(name: entry.key)).toList(); +} + +List _genSubRules(Map json) { + return json.entries + .map( + (entry) => SubRule( + name: entry.key, + ), + ) + .toList(); +} + @freezed class ClashConfigSnippet with _$ClashConfigSnippet { const factory ClashConfigSnippet({ @Default([]) @JsonKey(name: "proxy-groups") List proxyGroups, - @Default([]) List rule, + @JsonKey(fromJson: _genRule) @Default([]) List rule, + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + @Default([]) + List ruleProvider, + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + @Default([]) + List subRules, }) = _ClashConfigSnippet; factory ClashConfigSnippet.fromJson(Map json) => diff --git a/lib/models/common.dart b/lib/models/common.dart index 18f986f..10c60f0 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -382,7 +382,7 @@ extension ColorSchemesExt on ColorSchemes { ); } return lightColorScheme != null - ? ColorScheme.fromSeed(seedColor: lightColorScheme!.primary) + ? ColorScheme.fromSeed(seedColor: lightColorScheme!.primary,dynamicSchemeVariant: DynamicSchemeVariant.vibrant) : ColorScheme.fromSeed(seedColor: defaultPrimaryColor); } } @@ -481,13 +481,13 @@ class Field with _$Field { }) = _Field; } -enum ActionType { +enum PopupMenuItemType { primary, danger, } -class ActionItemData { - const ActionItemData({ +class PopupMenuItemData { + const PopupMenuItemData({ this.icon, required this.label, required this.onPressed, @@ -497,7 +497,7 @@ class ActionItemData { final double? iconSize; final String label; - final VoidCallback onPressed; + final VoidCallback? onPressed; final IconData? icon; - final ActionType? type; + final PopupMenuItemType? type; } \ No newline at end of file diff --git a/lib/models/config.dart b/lib/models/config.dart index be10068..bf5a5f3 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -37,7 +37,7 @@ const defaultProxiesStyle = ProxiesStyle(); const defaultWindowProps = WindowProps(); const defaultAccessControl = AccessControl(); final defaultThemeProps = ThemeProps().copyWith( - primaryColor: defaultPrimaryColor.value, + primaryColor: defaultPrimaryColor.toARGB32(), themeMode: ThemeMode.dark, ); @@ -121,7 +121,7 @@ extension AccessControlExt on AccessControl { @freezed class WindowProps with _$WindowProps { const factory WindowProps({ - @Default(900) double width, + @Default(750) double width, @Default(600) double height, double? top, double? left, diff --git a/lib/models/core.dart b/lib/models/core.dart index 8d9319b..04373c7 100644 --- a/lib/models/core.dart +++ b/lib/models/core.dart @@ -62,6 +62,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams { @JsonKey(name: "is-patch") required bool isPatch, @JsonKey(name: "selected-map") required SelectedMap selectedMap, @JsonKey(name: "override-dns") required bool overrideDns, + @JsonKey(name: "override-rule") required bool overrideRule, @JsonKey(name: "test-url") required String testUrl, }) = _ConfigExtendedParams; diff --git a/lib/models/generated/app.freezed.dart b/lib/models/generated/app.freezed.dart index b8b94b7..522245d 100644 --- a/lib/models/generated/app.freezed.dart +++ b/lib/models/generated/app.freezed.dart @@ -21,7 +21,7 @@ mixin _$AppState { List get packages => throw _privateConstructorUsedError; ColorSchemes get colorSchemes => throw _privateConstructorUsedError; int get sortNum => throw _privateConstructorUsedError; - double get viewWidth => throw _privateConstructorUsedError; + Size get viewSize => throw _privateConstructorUsedError; Map> get delayMap => throw _privateConstructorUsedError; List get groups => throw _privateConstructorUsedError; @@ -54,7 +54,7 @@ abstract class $AppStateCopyWith<$Res> { List packages, ColorSchemes colorSchemes, int sortNum, - double viewWidth, + Size viewSize, Map> delayMap, List groups, int checkIpNum, @@ -91,7 +91,7 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> Object? packages = null, Object? colorSchemes = null, Object? sortNum = null, - Object? viewWidth = null, + Object? viewSize = null, Object? delayMap = null, Object? groups = null, Object? checkIpNum = null, @@ -126,10 +126,10 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable as int, - viewWidth: null == viewWidth - ? _value.viewWidth - : viewWidth // ignore: cast_nullable_to_non_nullable - as double, + viewSize: null == viewSize + ? _value.viewSize + : viewSize // ignore: cast_nullable_to_non_nullable + as Size, delayMap: null == delayMap ? _value.delayMap : delayMap // ignore: cast_nullable_to_non_nullable @@ -206,7 +206,7 @@ abstract class _$$AppStateImplCopyWith<$Res> List packages, ColorSchemes colorSchemes, int sortNum, - double viewWidth, + Size viewSize, Map> delayMap, List groups, int checkIpNum, @@ -242,7 +242,7 @@ class __$$AppStateImplCopyWithImpl<$Res> Object? packages = null, Object? colorSchemes = null, Object? sortNum = null, - Object? viewWidth = null, + Object? viewSize = null, Object? delayMap = null, Object? groups = null, Object? checkIpNum = null, @@ -277,10 +277,10 @@ class __$$AppStateImplCopyWithImpl<$Res> ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable as int, - viewWidth: null == viewWidth - ? _value.viewWidth - : viewWidth // ignore: cast_nullable_to_non_nullable - as double, + viewSize: null == viewSize + ? _value.viewSize + : viewSize // ignore: cast_nullable_to_non_nullable + as Size, delayMap: null == delayMap ? _value._delayMap : delayMap // ignore: cast_nullable_to_non_nullable @@ -342,7 +342,7 @@ class _$AppStateImpl implements _AppState { final List packages = const [], this.colorSchemes = const ColorSchemes(), this.sortNum = 0, - required this.viewWidth, + required this.viewSize, final Map> delayMap = const {}, final List groups = const [], this.checkIpNum = 0, @@ -382,7 +382,7 @@ class _$AppStateImpl implements _AppState { @JsonKey() final int sortNum; @override - final double viewWidth; + final Size viewSize; final Map> _delayMap; @override @JsonKey() @@ -432,7 +432,7 @@ class _$AppStateImpl implements _AppState { @override String toString() { - return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, colorSchemes: $colorSchemes, sortNum: $sortNum, viewWidth: $viewWidth, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic)'; + 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)'; } @override @@ -447,8 +447,8 @@ class _$AppStateImpl implements _AppState { (identical(other.colorSchemes, colorSchemes) || other.colorSchemes == colorSchemes) && (identical(other.sortNum, sortNum) || other.sortNum == sortNum) && - (identical(other.viewWidth, viewWidth) || - other.viewWidth == viewWidth) && + (identical(other.viewSize, viewSize) || + other.viewSize == viewSize) && const DeepCollectionEquality().equals(other._delayMap, _delayMap) && const DeepCollectionEquality().equals(other._groups, _groups) && (identical(other.checkIpNum, checkIpNum) || @@ -477,7 +477,7 @@ class _$AppStateImpl implements _AppState { const DeepCollectionEquality().hash(_packages), colorSchemes, sortNum, - viewWidth, + viewSize, const DeepCollectionEquality().hash(_delayMap), const DeepCollectionEquality().hash(_groups), checkIpNum, @@ -507,7 +507,7 @@ abstract class _AppState implements AppState { final List packages, final ColorSchemes colorSchemes, final int sortNum, - required final double viewWidth, + required final Size viewSize, final Map> delayMap, final List groups, final int checkIpNum, @@ -532,7 +532,7 @@ abstract class _AppState implements AppState { @override int get sortNum; @override - double get viewWidth; + Size get viewSize; @override Map> get delayMap; @override diff --git a/lib/models/generated/clash_config.freezed.dart b/lib/models/generated/clash_config.freezed.dart index 0a6a9b5..1f851b2 100644 --- a/lib/models/generated/clash_config.freezed.dart +++ b/lib/models/generated/clash_config.freezed.dart @@ -37,7 +37,7 @@ mixin _$ProxyGroup { @JsonKey(name: "exclude-type") String? get excludeType => throw _privateConstructorUsedError; @JsonKey(name: "expected-status") - int? get expectedStatus => throw _privateConstructorUsedError; + dynamic get expectedStatus => throw _privateConstructorUsedError; bool? get hidden => throw _privateConstructorUsedError; String? get icon => throw _privateConstructorUsedError; @@ -70,7 +70,7 @@ abstract class $ProxyGroupCopyWith<$Res> { String? filter, @JsonKey(name: "expected-filter") String? excludeFilter, @JsonKey(name: "exclude-type") String? excludeType, - @JsonKey(name: "expected-status") int? expectedStatus, + @JsonKey(name: "expected-status") dynamic expectedStatus, bool? hidden, String? icon}); } @@ -158,7 +158,7 @@ class _$ProxyGroupCopyWithImpl<$Res, $Val extends ProxyGroup> expectedStatus: freezed == expectedStatus ? _value.expectedStatus : expectedStatus // ignore: cast_nullable_to_non_nullable - as int?, + as dynamic, hidden: freezed == hidden ? _value.hidden : hidden // ignore: cast_nullable_to_non_nullable @@ -192,7 +192,7 @@ abstract class _$$ProxyGroupImplCopyWith<$Res> String? filter, @JsonKey(name: "expected-filter") String? excludeFilter, @JsonKey(name: "exclude-type") String? excludeType, - @JsonKey(name: "expected-status") int? expectedStatus, + @JsonKey(name: "expected-status") dynamic expectedStatus, bool? hidden, String? icon}); } @@ -278,7 +278,7 @@ class __$$ProxyGroupImplCopyWithImpl<$Res> expectedStatus: freezed == expectedStatus ? _value.expectedStatus : expectedStatus // ignore: cast_nullable_to_non_nullable - as int?, + as dynamic, hidden: freezed == hidden ? _value.hidden : hidden // ignore: cast_nullable_to_non_nullable @@ -362,7 +362,7 @@ class _$ProxyGroupImpl implements _ProxyGroup { final String? excludeType; @override @JsonKey(name: "expected-status") - final int? expectedStatus; + final dynamic expectedStatus; @override final bool? hidden; @override @@ -394,8 +394,8 @@ class _$ProxyGroupImpl implements _ProxyGroup { other.excludeFilter == excludeFilter) && (identical(other.excludeType, excludeType) || other.excludeType == excludeType) && - (identical(other.expectedStatus, expectedStatus) || - other.expectedStatus == expectedStatus) && + const DeepCollectionEquality() + .equals(other.expectedStatus, expectedStatus) && (identical(other.hidden, hidden) || other.hidden == hidden) && (identical(other.icon, icon) || other.icon == icon)); } @@ -416,7 +416,7 @@ class _$ProxyGroupImpl implements _ProxyGroup { filter, excludeFilter, excludeType, - expectedStatus, + const DeepCollectionEquality().hash(expectedStatus), hidden, icon); @@ -451,7 +451,7 @@ abstract class _ProxyGroup implements ProxyGroup { final String? filter, @JsonKey(name: "expected-filter") final String? excludeFilter, @JsonKey(name: "exclude-type") final String? excludeType, - @JsonKey(name: "expected-status") final int? expectedStatus, + @JsonKey(name: "expected-status") final dynamic expectedStatus, final bool? hidden, final String? icon}) = _$ProxyGroupImpl; @@ -488,7 +488,7 @@ abstract class _ProxyGroup implements ProxyGroup { String? get excludeType; @override @JsonKey(name: "expected-status") - int? get expectedStatus; + dynamic get expectedStatus; @override bool? get hidden; @override @@ -502,6 +502,156 @@ abstract class _ProxyGroup implements ProxyGroup { throw _privateConstructorUsedError; } +RuleProvider _$RuleProviderFromJson(Map json) { + return _RuleProvider.fromJson(json); +} + +/// @nodoc +mixin _$RuleProvider { + String get name => throw _privateConstructorUsedError; + + /// Serializes this RuleProvider to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RuleProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RuleProviderCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RuleProviderCopyWith<$Res> { + factory $RuleProviderCopyWith( + RuleProvider value, $Res Function(RuleProvider) then) = + _$RuleProviderCopyWithImpl<$Res, RuleProvider>; + @useResult + $Res call({String name}); +} + +/// @nodoc +class _$RuleProviderCopyWithImpl<$Res, $Val extends RuleProvider> + implements $RuleProviderCopyWith<$Res> { + _$RuleProviderCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RuleProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RuleProviderImplCopyWith<$Res> + implements $RuleProviderCopyWith<$Res> { + factory _$$RuleProviderImplCopyWith( + _$RuleProviderImpl value, $Res Function(_$RuleProviderImpl) then) = + __$$RuleProviderImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name}); +} + +/// @nodoc +class __$$RuleProviderImplCopyWithImpl<$Res> + extends _$RuleProviderCopyWithImpl<$Res, _$RuleProviderImpl> + implements _$$RuleProviderImplCopyWith<$Res> { + __$$RuleProviderImplCopyWithImpl( + _$RuleProviderImpl _value, $Res Function(_$RuleProviderImpl) _then) + : super(_value, _then); + + /// Create a copy of RuleProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + }) { + return _then(_$RuleProviderImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RuleProviderImpl implements _RuleProvider { + const _$RuleProviderImpl({required this.name}); + + factory _$RuleProviderImpl.fromJson(Map json) => + _$$RuleProviderImplFromJson(json); + + @override + final String name; + + @override + String toString() { + return 'RuleProvider(name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RuleProviderImpl && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name); + + /// Create a copy of RuleProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RuleProviderImplCopyWith<_$RuleProviderImpl> get copyWith => + __$$RuleProviderImplCopyWithImpl<_$RuleProviderImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RuleProviderImplToJson( + this, + ); + } +} + +abstract class _RuleProvider implements RuleProvider { + const factory _RuleProvider({required final String name}) = + _$RuleProviderImpl; + + factory _RuleProvider.fromJson(Map json) = + _$RuleProviderImpl.fromJson; + + @override + String get name; + + /// Create a copy of RuleProvider + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RuleProviderImplCopyWith<_$RuleProviderImpl> get copyWith => + throw _privateConstructorUsedError; +} + Tun _$TunFromJson(Map json) { return _Tun.fromJson(json); } @@ -1834,6 +1984,571 @@ abstract class _GeoXUrl implements GeoXUrl { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$ParsedRule { + RuleAction get ruleAction => throw _privateConstructorUsedError; + String? get content => throw _privateConstructorUsedError; + String? get ruleTarget => throw _privateConstructorUsedError; + String? get ruleProvider => throw _privateConstructorUsedError; + String? get subRule => throw _privateConstructorUsedError; + bool get noResolve => throw _privateConstructorUsedError; + bool get src => throw _privateConstructorUsedError; + + /// Create a copy of ParsedRule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ParsedRuleCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ParsedRuleCopyWith<$Res> { + factory $ParsedRuleCopyWith( + ParsedRule value, $Res Function(ParsedRule) then) = + _$ParsedRuleCopyWithImpl<$Res, ParsedRule>; + @useResult + $Res call( + {RuleAction ruleAction, + String? content, + String? ruleTarget, + String? ruleProvider, + String? subRule, + bool noResolve, + bool src}); +} + +/// @nodoc +class _$ParsedRuleCopyWithImpl<$Res, $Val extends ParsedRule> + implements $ParsedRuleCopyWith<$Res> { + _$ParsedRuleCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ParsedRule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? ruleAction = null, + Object? content = freezed, + Object? ruleTarget = freezed, + Object? ruleProvider = freezed, + Object? subRule = freezed, + Object? noResolve = null, + Object? src = null, + }) { + return _then(_value.copyWith( + ruleAction: null == ruleAction + ? _value.ruleAction + : ruleAction // ignore: cast_nullable_to_non_nullable + as RuleAction, + content: freezed == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String?, + ruleTarget: freezed == ruleTarget + ? _value.ruleTarget + : ruleTarget // ignore: cast_nullable_to_non_nullable + as String?, + ruleProvider: freezed == ruleProvider + ? _value.ruleProvider + : ruleProvider // ignore: cast_nullable_to_non_nullable + as String?, + subRule: freezed == subRule + ? _value.subRule + : subRule // ignore: cast_nullable_to_non_nullable + as String?, + noResolve: null == noResolve + ? _value.noResolve + : noResolve // ignore: cast_nullable_to_non_nullable + as bool, + src: null == src + ? _value.src + : src // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ParsedRuleImplCopyWith<$Res> + implements $ParsedRuleCopyWith<$Res> { + factory _$$ParsedRuleImplCopyWith( + _$ParsedRuleImpl value, $Res Function(_$ParsedRuleImpl) then) = + __$$ParsedRuleImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {RuleAction ruleAction, + String? content, + String? ruleTarget, + String? ruleProvider, + String? subRule, + bool noResolve, + bool src}); +} + +/// @nodoc +class __$$ParsedRuleImplCopyWithImpl<$Res> + extends _$ParsedRuleCopyWithImpl<$Res, _$ParsedRuleImpl> + implements _$$ParsedRuleImplCopyWith<$Res> { + __$$ParsedRuleImplCopyWithImpl( + _$ParsedRuleImpl _value, $Res Function(_$ParsedRuleImpl) _then) + : super(_value, _then); + + /// Create a copy of ParsedRule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? ruleAction = null, + Object? content = freezed, + Object? ruleTarget = freezed, + Object? ruleProvider = freezed, + Object? subRule = freezed, + Object? noResolve = null, + Object? src = null, + }) { + return _then(_$ParsedRuleImpl( + ruleAction: null == ruleAction + ? _value.ruleAction + : ruleAction // ignore: cast_nullable_to_non_nullable + as RuleAction, + content: freezed == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String?, + ruleTarget: freezed == ruleTarget + ? _value.ruleTarget + : ruleTarget // ignore: cast_nullable_to_non_nullable + as String?, + ruleProvider: freezed == ruleProvider + ? _value.ruleProvider + : ruleProvider // ignore: cast_nullable_to_non_nullable + as String?, + subRule: freezed == subRule + ? _value.subRule + : subRule // ignore: cast_nullable_to_non_nullable + as String?, + noResolve: null == noResolve + ? _value.noResolve + : noResolve // ignore: cast_nullable_to_non_nullable + as bool, + src: null == src + ? _value.src + : src // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$ParsedRuleImpl implements _ParsedRule { + const _$ParsedRuleImpl( + {required this.ruleAction, + this.content, + this.ruleTarget, + this.ruleProvider, + this.subRule, + this.noResolve = false, + this.src = false}); + + @override + final RuleAction ruleAction; + @override + final String? content; + @override + final String? ruleTarget; + @override + final String? ruleProvider; + @override + final String? subRule; + @override + @JsonKey() + final bool noResolve; + @override + @JsonKey() + final bool src; + + @override + String toString() { + return 'ParsedRule(ruleAction: $ruleAction, content: $content, ruleTarget: $ruleTarget, ruleProvider: $ruleProvider, subRule: $subRule, noResolve: $noResolve, src: $src)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ParsedRuleImpl && + (identical(other.ruleAction, ruleAction) || + other.ruleAction == ruleAction) && + (identical(other.content, content) || other.content == content) && + (identical(other.ruleTarget, ruleTarget) || + other.ruleTarget == ruleTarget) && + (identical(other.ruleProvider, ruleProvider) || + other.ruleProvider == ruleProvider) && + (identical(other.subRule, subRule) || other.subRule == subRule) && + (identical(other.noResolve, noResolve) || + other.noResolve == noResolve) && + (identical(other.src, src) || other.src == src)); + } + + @override + int get hashCode => Object.hash(runtimeType, ruleAction, content, ruleTarget, + ruleProvider, subRule, noResolve, src); + + /// Create a copy of ParsedRule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ParsedRuleImplCopyWith<_$ParsedRuleImpl> get copyWith => + __$$ParsedRuleImplCopyWithImpl<_$ParsedRuleImpl>(this, _$identity); +} + +abstract class _ParsedRule implements ParsedRule { + const factory _ParsedRule( + {required final RuleAction ruleAction, + final String? content, + final String? ruleTarget, + final String? ruleProvider, + final String? subRule, + final bool noResolve, + final bool src}) = _$ParsedRuleImpl; + + @override + RuleAction get ruleAction; + @override + String? get content; + @override + String? get ruleTarget; + @override + String? get ruleProvider; + @override + String? get subRule; + @override + bool get noResolve; + @override + bool get src; + + /// Create a copy of ParsedRule + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ParsedRuleImplCopyWith<_$ParsedRuleImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Rule _$RuleFromJson(Map json) { + return _Rule.fromJson(json); +} + +/// @nodoc +mixin _$Rule { + String get id => throw _privateConstructorUsedError; + String get value => throw _privateConstructorUsedError; + + /// Serializes this Rule to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Rule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RuleCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RuleCopyWith<$Res> { + factory $RuleCopyWith(Rule value, $Res Function(Rule) then) = + _$RuleCopyWithImpl<$Res, Rule>; + @useResult + $Res call({String id, String value}); +} + +/// @nodoc +class _$RuleCopyWithImpl<$Res, $Val extends Rule> + implements $RuleCopyWith<$Res> { + _$RuleCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Rule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? value = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + value: null == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RuleImplCopyWith<$Res> implements $RuleCopyWith<$Res> { + factory _$$RuleImplCopyWith( + _$RuleImpl value, $Res Function(_$RuleImpl) then) = + __$$RuleImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String value}); +} + +/// @nodoc +class __$$RuleImplCopyWithImpl<$Res> + extends _$RuleCopyWithImpl<$Res, _$RuleImpl> + implements _$$RuleImplCopyWith<$Res> { + __$$RuleImplCopyWithImpl(_$RuleImpl _value, $Res Function(_$RuleImpl) _then) + : super(_value, _then); + + /// Create a copy of Rule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? value = null, + }) { + return _then(_$RuleImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + value: null == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RuleImpl implements _Rule { + const _$RuleImpl({required this.id, required this.value}); + + factory _$RuleImpl.fromJson(Map json) => + _$$RuleImplFromJson(json); + + @override + final String id; + @override + final String value; + + @override + String toString() { + return 'Rule(id: $id, value: $value)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RuleImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.value, value) || other.value == value)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, value); + + /// Create a copy of Rule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RuleImplCopyWith<_$RuleImpl> get copyWith => + __$$RuleImplCopyWithImpl<_$RuleImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RuleImplToJson( + this, + ); + } +} + +abstract class _Rule implements Rule { + const factory _Rule({required final String id, required final String value}) = + _$RuleImpl; + + factory _Rule.fromJson(Map json) = _$RuleImpl.fromJson; + + @override + String get id; + @override + String get value; + + /// Create a copy of Rule + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RuleImplCopyWith<_$RuleImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SubRule _$SubRuleFromJson(Map json) { + return _SubRule.fromJson(json); +} + +/// @nodoc +mixin _$SubRule { + String get name => throw _privateConstructorUsedError; + + /// Serializes this SubRule to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SubRule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SubRuleCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SubRuleCopyWith<$Res> { + factory $SubRuleCopyWith(SubRule value, $Res Function(SubRule) then) = + _$SubRuleCopyWithImpl<$Res, SubRule>; + @useResult + $Res call({String name}); +} + +/// @nodoc +class _$SubRuleCopyWithImpl<$Res, $Val extends SubRule> + implements $SubRuleCopyWith<$Res> { + _$SubRuleCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SubRule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SubRuleImplCopyWith<$Res> implements $SubRuleCopyWith<$Res> { + factory _$$SubRuleImplCopyWith( + _$SubRuleImpl value, $Res Function(_$SubRuleImpl) then) = + __$$SubRuleImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name}); +} + +/// @nodoc +class __$$SubRuleImplCopyWithImpl<$Res> + extends _$SubRuleCopyWithImpl<$Res, _$SubRuleImpl> + implements _$$SubRuleImplCopyWith<$Res> { + __$$SubRuleImplCopyWithImpl( + _$SubRuleImpl _value, $Res Function(_$SubRuleImpl) _then) + : super(_value, _then); + + /// Create a copy of SubRule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + }) { + return _then(_$SubRuleImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SubRuleImpl implements _SubRule { + const _$SubRuleImpl({required this.name}); + + factory _$SubRuleImpl.fromJson(Map json) => + _$$SubRuleImplFromJson(json); + + @override + final String name; + + @override + String toString() { + return 'SubRule(name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SubRuleImpl && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name); + + /// Create a copy of SubRule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SubRuleImplCopyWith<_$SubRuleImpl> get copyWith => + __$$SubRuleImplCopyWithImpl<_$SubRuleImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SubRuleImplToJson( + this, + ); + } +} + +abstract class _SubRule implements SubRule { + const factory _SubRule({required final String name}) = _$SubRuleImpl; + + factory _SubRule.fromJson(Map json) = _$SubRuleImpl.fromJson; + + @override + String get name; + + /// Create a copy of SubRule + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SubRuleImplCopyWith<_$SubRuleImpl> get copyWith => + throw _privateConstructorUsedError; +} + ClashConfigSnippet _$ClashConfigSnippetFromJson(Map json) { return _ClashConfigSnippet.fromJson(json); } @@ -1842,7 +2557,12 @@ ClashConfigSnippet _$ClashConfigSnippetFromJson(Map json) { mixin _$ClashConfigSnippet { @JsonKey(name: "proxy-groups") List get proxyGroups => throw _privateConstructorUsedError; - List get rule => throw _privateConstructorUsedError; + @JsonKey(fromJson: _genRule) + List get rule => throw _privateConstructorUsedError; + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + List get ruleProvider => throw _privateConstructorUsedError; + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + List get subRules => throw _privateConstructorUsedError; /// Serializes this ClashConfigSnippet to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1862,7 +2582,11 @@ abstract class $ClashConfigSnippetCopyWith<$Res> { @useResult $Res call( {@JsonKey(name: "proxy-groups") List proxyGroups, - List rule}); + @JsonKey(fromJson: _genRule) List rule, + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + List ruleProvider, + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + List subRules}); } /// @nodoc @@ -1882,6 +2606,8 @@ class _$ClashConfigSnippetCopyWithImpl<$Res, $Val extends ClashConfigSnippet> $Res call({ Object? proxyGroups = null, Object? rule = null, + Object? ruleProvider = null, + Object? subRules = null, }) { return _then(_value.copyWith( proxyGroups: null == proxyGroups @@ -1891,7 +2617,15 @@ class _$ClashConfigSnippetCopyWithImpl<$Res, $Val extends ClashConfigSnippet> rule: null == rule ? _value.rule : rule // ignore: cast_nullable_to_non_nullable - as List, + as List, + ruleProvider: null == ruleProvider + ? _value.ruleProvider + : ruleProvider // ignore: cast_nullable_to_non_nullable + as List, + subRules: null == subRules + ? _value.subRules + : subRules // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -1906,7 +2640,11 @@ abstract class _$$ClashConfigSnippetImplCopyWith<$Res> @useResult $Res call( {@JsonKey(name: "proxy-groups") List proxyGroups, - List rule}); + @JsonKey(fromJson: _genRule) List rule, + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + List ruleProvider, + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + List subRules}); } /// @nodoc @@ -1924,6 +2662,8 @@ class __$$ClashConfigSnippetImplCopyWithImpl<$Res> $Res call({ Object? proxyGroups = null, Object? rule = null, + Object? ruleProvider = null, + Object? subRules = null, }) { return _then(_$ClashConfigSnippetImpl( proxyGroups: null == proxyGroups @@ -1933,7 +2673,15 @@ class __$$ClashConfigSnippetImplCopyWithImpl<$Res> rule: null == rule ? _value._rule : rule // ignore: cast_nullable_to_non_nullable - as List, + as List, + ruleProvider: null == ruleProvider + ? _value._ruleProvider + : ruleProvider // ignore: cast_nullable_to_non_nullable + as List, + subRules: null == subRules + ? _value._subRules + : subRules // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -1944,9 +2692,15 @@ class _$ClashConfigSnippetImpl implements _ClashConfigSnippet { const _$ClashConfigSnippetImpl( {@JsonKey(name: "proxy-groups") final List proxyGroups = const [], - final List rule = const []}) + @JsonKey(fromJson: _genRule) final List rule = const [], + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + final List ruleProvider = const [], + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + final List subRules = const []}) : _proxyGroups = proxyGroups, - _rule = rule; + _rule = rule, + _ruleProvider = ruleProvider, + _subRules = subRules; factory _$ClashConfigSnippetImpl.fromJson(Map json) => _$$ClashConfigSnippetImplFromJson(json); @@ -1960,18 +2714,36 @@ class _$ClashConfigSnippetImpl implements _ClashConfigSnippet { return EqualUnmodifiableListView(_proxyGroups); } - final List _rule; + final List _rule; @override - @JsonKey() - List get rule { + @JsonKey(fromJson: _genRule) + List get rule { if (_rule is EqualUnmodifiableListView) return _rule; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_rule); } + final List _ruleProvider; + @override + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + List get ruleProvider { + if (_ruleProvider is EqualUnmodifiableListView) return _ruleProvider; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_ruleProvider); + } + + final List _subRules; + @override + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + List get subRules { + if (_subRules is EqualUnmodifiableListView) return _subRules; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_subRules); + } + @override String toString() { - return 'ClashConfigSnippet(proxyGroups: $proxyGroups, rule: $rule)'; + return 'ClashConfigSnippet(proxyGroups: $proxyGroups, rule: $rule, ruleProvider: $ruleProvider, subRules: $subRules)'; } @override @@ -1981,7 +2753,10 @@ class _$ClashConfigSnippetImpl implements _ClashConfigSnippet { other is _$ClashConfigSnippetImpl && const DeepCollectionEquality() .equals(other._proxyGroups, _proxyGroups) && - const DeepCollectionEquality().equals(other._rule, _rule)); + const DeepCollectionEquality().equals(other._rule, _rule) && + const DeepCollectionEquality() + .equals(other._ruleProvider, _ruleProvider) && + const DeepCollectionEquality().equals(other._subRules, _subRules)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -1989,7 +2764,9 @@ class _$ClashConfigSnippetImpl implements _ClashConfigSnippet { int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_proxyGroups), - const DeepCollectionEquality().hash(_rule)); + const DeepCollectionEquality().hash(_rule), + const DeepCollectionEquality().hash(_ruleProvider), + const DeepCollectionEquality().hash(_subRules)); /// Create a copy of ClashConfigSnippet /// with the given fields replaced by the non-null parameter values. @@ -2011,7 +2788,11 @@ class _$ClashConfigSnippetImpl implements _ClashConfigSnippet { abstract class _ClashConfigSnippet implements ClashConfigSnippet { const factory _ClashConfigSnippet( {@JsonKey(name: "proxy-groups") final List proxyGroups, - final List rule}) = _$ClashConfigSnippetImpl; + @JsonKey(fromJson: _genRule) final List rule, + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + final List ruleProvider, + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + final List subRules}) = _$ClashConfigSnippetImpl; factory _ClashConfigSnippet.fromJson(Map json) = _$ClashConfigSnippetImpl.fromJson; @@ -2020,7 +2801,14 @@ abstract class _ClashConfigSnippet implements ClashConfigSnippet { @JsonKey(name: "proxy-groups") List get proxyGroups; @override - List get rule; + @JsonKey(fromJson: _genRule) + List get rule; + @override + @JsonKey(name: "rule-providers", fromJson: _genRuleProviders) + List get ruleProvider; + @override + @JsonKey(name: "sub-rules", fromJson: _genSubRules) + List get subRules; /// Create a copy of ClashConfigSnippet /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 1d4ac49..61208c3 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -21,7 +21,7 @@ _$ProxyGroupImpl _$$ProxyGroupImplFromJson(Map json) => filter: json['filter'] as String?, excludeFilter: json['expected-filter'] as String?, excludeType: json['exclude-type'] as String?, - expectedStatus: (json['expected-status'] as num?)?.toInt(), + expectedStatus: json['expected-status'], hidden: json['hidden'] as bool?, icon: json['icon'] as String?, ); @@ -53,6 +53,16 @@ const _$GroupTypeEnumMap = { GroupType.Relay: 'Relay', }; +_$RuleProviderImpl _$$RuleProviderImplFromJson(Map json) => + _$RuleProviderImpl( + name: json['name'] as String, + ); + +Map _$$RuleProviderImplToJson(_$RuleProviderImpl instance) => + { + 'name': instance.name, + }; + _$TunImpl _$$TunImplFromJson(Map json) => _$TunImpl( enable: json['enable'] as bool? ?? false, device: json['device'] as String? ?? appName, @@ -206,6 +216,27 @@ Map _$$GeoXUrlImplToJson(_$GeoXUrlImpl instance) => 'geosite': instance.geosite, }; +_$RuleImpl _$$RuleImplFromJson(Map json) => _$RuleImpl( + id: json['id'] as String, + value: json['value'] as String, + ); + +Map _$$RuleImplToJson(_$RuleImpl instance) => + { + 'id': instance.id, + 'value': instance.value, + }; + +_$SubRuleImpl _$$SubRuleImplFromJson(Map json) => + _$SubRuleImpl( + name: json['name'] as String, + ); + +Map _$$SubRuleImplToJson(_$SubRuleImpl instance) => + { + 'name': instance.name, + }; + _$ClashConfigSnippetImpl _$$ClashConfigSnippetImplFromJson( Map json) => _$ClashConfigSnippetImpl( @@ -213,9 +244,13 @@ _$ClashConfigSnippetImpl _$$ClashConfigSnippetImplFromJson( ?.map((e) => ProxyGroup.fromJson(e as Map)) .toList() ?? const [], - rule: - (json['rule'] as List?)?.map((e) => e as String).toList() ?? - const [], + rule: json['rule'] == null ? const [] : _genRule(json['rule'] as List?), + ruleProvider: json['rule-providers'] == null + ? const [] + : _genRuleProviders(json['rule-providers'] as Map), + subRules: json['sub-rules'] == null + ? const [] + : _genSubRules(json['sub-rules'] as Map), ); Map _$$ClashConfigSnippetImplToJson( @@ -223,6 +258,8 @@ Map _$$ClashConfigSnippetImplToJson( { 'proxy-groups': instance.proxyGroups, 'rule': instance.rule, + 'rule-providers': instance.ruleProvider, + 'sub-rules': instance.subRules, }; _$ClashConfigImpl _$$ClashConfigImplFromJson(Map json) => diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 8ee0a2b..069eb4f 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -912,7 +912,7 @@ class __$$WindowPropsImplCopyWithImpl<$Res> @JsonSerializable() class _$WindowPropsImpl implements _WindowProps { const _$WindowPropsImpl( - {this.width = 900, this.height = 600, this.top, this.left}); + {this.width = 750, this.height = 600, this.top, this.left}); factory _$WindowPropsImpl.fromJson(Map json) => _$$WindowPropsImplFromJson(json); diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index de4e17e..6a12be9 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -102,7 +102,7 @@ const _$AccessSortTypeEnumMap = { _$WindowPropsImpl _$$WindowPropsImplFromJson(Map json) => _$WindowPropsImpl( - width: (json['width'] as num?)?.toDouble() ?? 900, + width: (json['width'] as num?)?.toDouble() ?? 750, height: (json['height'] as num?)?.toDouble() ?? 600, top: (json['top'] as num?)?.toDouble(), left: (json['left'] as num?)?.toDouble(), diff --git a/lib/models/generated/core.freezed.dart b/lib/models/generated/core.freezed.dart index 7a6875f..d214e99 100644 --- a/lib/models/generated/core.freezed.dart +++ b/lib/models/generated/core.freezed.dart @@ -667,6 +667,8 @@ mixin _$ConfigExtendedParams { Map get selectedMap => throw _privateConstructorUsedError; @JsonKey(name: "override-dns") bool get overrideDns => throw _privateConstructorUsedError; + @JsonKey(name: "override-rule") + bool get overrideRule => throw _privateConstructorUsedError; @JsonKey(name: "test-url") String get testUrl => throw _privateConstructorUsedError; @@ -690,6 +692,7 @@ abstract class $ConfigExtendedParamsCopyWith<$Res> { {@JsonKey(name: "is-patch") bool isPatch, @JsonKey(name: "selected-map") Map selectedMap, @JsonKey(name: "override-dns") bool overrideDns, + @JsonKey(name: "override-rule") bool overrideRule, @JsonKey(name: "test-url") String testUrl}); } @@ -712,6 +715,7 @@ class _$ConfigExtendedParamsCopyWithImpl<$Res, Object? isPatch = null, Object? selectedMap = null, Object? overrideDns = null, + Object? overrideRule = null, Object? testUrl = null, }) { return _then(_value.copyWith( @@ -727,6 +731,10 @@ class _$ConfigExtendedParamsCopyWithImpl<$Res, ? _value.overrideDns : overrideDns // ignore: cast_nullable_to_non_nullable as bool, + overrideRule: null == overrideRule + ? _value.overrideRule + : overrideRule // ignore: cast_nullable_to_non_nullable + as bool, testUrl: null == testUrl ? _value.testUrl : testUrl // ignore: cast_nullable_to_non_nullable @@ -747,6 +755,7 @@ abstract class _$$ConfigExtendedParamsImplCopyWith<$Res> {@JsonKey(name: "is-patch") bool isPatch, @JsonKey(name: "selected-map") Map selectedMap, @JsonKey(name: "override-dns") bool overrideDns, + @JsonKey(name: "override-rule") bool overrideRule, @JsonKey(name: "test-url") String testUrl}); } @@ -766,6 +775,7 @@ class __$$ConfigExtendedParamsImplCopyWithImpl<$Res> Object? isPatch = null, Object? selectedMap = null, Object? overrideDns = null, + Object? overrideRule = null, Object? testUrl = null, }) { return _then(_$ConfigExtendedParamsImpl( @@ -781,6 +791,10 @@ class __$$ConfigExtendedParamsImplCopyWithImpl<$Res> ? _value.overrideDns : overrideDns // ignore: cast_nullable_to_non_nullable as bool, + overrideRule: null == overrideRule + ? _value.overrideRule + : overrideRule // ignore: cast_nullable_to_non_nullable + as bool, testUrl: null == testUrl ? _value.testUrl : testUrl // ignore: cast_nullable_to_non_nullable @@ -797,6 +811,7 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { @JsonKey(name: "selected-map") required final Map selectedMap, @JsonKey(name: "override-dns") required this.overrideDns, + @JsonKey(name: "override-rule") required this.overrideRule, @JsonKey(name: "test-url") required this.testUrl}) : _selectedMap = selectedMap; @@ -819,12 +834,15 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { @JsonKey(name: "override-dns") final bool overrideDns; @override + @JsonKey(name: "override-rule") + final bool overrideRule; + @override @JsonKey(name: "test-url") final String testUrl; @override String toString() { - return 'ConfigExtendedParams(isPatch: $isPatch, selectedMap: $selectedMap, overrideDns: $overrideDns, testUrl: $testUrl)'; + return 'ConfigExtendedParams(isPatch: $isPatch, selectedMap: $selectedMap, overrideDns: $overrideDns, overrideRule: $overrideRule, testUrl: $testUrl)'; } @override @@ -837,13 +855,20 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { .equals(other._selectedMap, _selectedMap) && (identical(other.overrideDns, overrideDns) || other.overrideDns == overrideDns) && + (identical(other.overrideRule, overrideRule) || + other.overrideRule == overrideRule) && (identical(other.testUrl, testUrl) || other.testUrl == testUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, isPatch, - const DeepCollectionEquality().hash(_selectedMap), overrideDns, testUrl); + int get hashCode => Object.hash( + runtimeType, + isPatch, + const DeepCollectionEquality().hash(_selectedMap), + overrideDns, + overrideRule, + testUrl); /// Create a copy of ConfigExtendedParams /// with the given fields replaced by the non-null parameter values. @@ -869,6 +894,7 @@ abstract class _ConfigExtendedParams implements ConfigExtendedParams { @JsonKey(name: "selected-map") required final Map selectedMap, @JsonKey(name: "override-dns") required final bool overrideDns, + @JsonKey(name: "override-rule") required final bool overrideRule, @JsonKey(name: "test-url") required final String testUrl}) = _$ConfigExtendedParamsImpl; @@ -885,6 +911,9 @@ abstract class _ConfigExtendedParams implements ConfigExtendedParams { @JsonKey(name: "override-dns") bool get overrideDns; @override + @JsonKey(name: "override-rule") + bool get overrideRule; + @override @JsonKey(name: "test-url") String get testUrl; diff --git a/lib/models/generated/core.g.dart b/lib/models/generated/core.g.dart index 3dd83ad..97533d6 100644 --- a/lib/models/generated/core.g.dart +++ b/lib/models/generated/core.g.dart @@ -69,6 +69,7 @@ _$ConfigExtendedParamsImpl _$$ConfigExtendedParamsImplFromJson( isPatch: json['is-patch'] as bool, selectedMap: Map.from(json['selected-map'] as Map), overrideDns: json['override-dns'] as bool, + overrideRule: json['override-rule'] as bool, testUrl: json['test-url'] as String, ); @@ -78,6 +79,7 @@ Map _$$ConfigExtendedParamsImplToJson( 'is-patch': instance.isPatch, 'selected-map': instance.selectedMap, 'override-dns': instance.overrideDns, + 'override-rule': instance.overrideRule, 'test-url': instance.testUrl, }; diff --git a/lib/models/generated/profile.freezed.dart b/lib/models/generated/profile.freezed.dart index 0dc3aca..4919406 100644 --- a/lib/models/generated/profile.freezed.dart +++ b/lib/models/generated/profile.freezed.dart @@ -238,6 +238,7 @@ mixin _$Profile { bool get autoUpdate => throw _privateConstructorUsedError; Map get selectedMap => throw _privateConstructorUsedError; Set get unfoldSet => throw _privateConstructorUsedError; + OverrideData get overrideData => throw _privateConstructorUsedError; @JsonKey(includeToJson: false, includeFromJson: false) bool get isUpdating => throw _privateConstructorUsedError; @@ -266,9 +267,11 @@ abstract class $ProfileCopyWith<$Res> { bool autoUpdate, Map selectedMap, Set unfoldSet, + OverrideData overrideData, @JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating}); $SubscriptionInfoCopyWith<$Res>? get subscriptionInfo; + $OverrideDataCopyWith<$Res> get overrideData; } /// @nodoc @@ -296,6 +299,7 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> Object? autoUpdate = null, Object? selectedMap = null, Object? unfoldSet = null, + Object? overrideData = null, Object? isUpdating = null, }) { return _then(_value.copyWith( @@ -339,6 +343,10 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> ? _value.unfoldSet : unfoldSet // ignore: cast_nullable_to_non_nullable as Set, + overrideData: null == overrideData + ? _value.overrideData + : overrideData // ignore: cast_nullable_to_non_nullable + as OverrideData, isUpdating: null == isUpdating ? _value.isUpdating : isUpdating // ignore: cast_nullable_to_non_nullable @@ -359,6 +367,16 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> return _then(_value.copyWith(subscriptionInfo: value) as $Val); }); } + + /// Create a copy of Profile + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OverrideDataCopyWith<$Res> get overrideData { + return $OverrideDataCopyWith<$Res>(_value.overrideData, (value) { + return _then(_value.copyWith(overrideData: value) as $Val); + }); + } } /// @nodoc @@ -379,10 +397,13 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> { bool autoUpdate, Map selectedMap, Set unfoldSet, + OverrideData overrideData, @JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating}); @override $SubscriptionInfoCopyWith<$Res>? get subscriptionInfo; + @override + $OverrideDataCopyWith<$Res> get overrideData; } /// @nodoc @@ -408,6 +429,7 @@ class __$$ProfileImplCopyWithImpl<$Res> Object? autoUpdate = null, Object? selectedMap = null, Object? unfoldSet = null, + Object? overrideData = null, Object? isUpdating = null, }) { return _then(_$ProfileImpl( @@ -451,6 +473,10 @@ class __$$ProfileImplCopyWithImpl<$Res> ? _value._unfoldSet : unfoldSet // ignore: cast_nullable_to_non_nullable as Set, + overrideData: null == overrideData + ? _value.overrideData + : overrideData // ignore: cast_nullable_to_non_nullable + as OverrideData, isUpdating: null == isUpdating ? _value.isUpdating : isUpdating // ignore: cast_nullable_to_non_nullable @@ -473,6 +499,7 @@ class _$ProfileImpl implements _Profile { this.autoUpdate = true, final Map selectedMap = const {}, final Set unfoldSet = const {}, + this.overrideData = const OverrideData(), @JsonKey(includeToJson: false, includeFromJson: false) this.isUpdating = false}) : _selectedMap = selectedMap, @@ -517,13 +544,16 @@ class _$ProfileImpl implements _Profile { return EqualUnmodifiableSetView(_unfoldSet); } + @override + @JsonKey() + final OverrideData overrideData; @override @JsonKey(includeToJson: false, includeFromJson: false) final bool isUpdating; @override String toString() { - return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, subscriptionInfo: $subscriptionInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet, isUpdating: $isUpdating)'; + return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, subscriptionInfo: $subscriptionInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet, overrideData: $overrideData, isUpdating: $isUpdating)'; } @override @@ -548,6 +578,8 @@ class _$ProfileImpl implements _Profile { .equals(other._selectedMap, _selectedMap) && const DeepCollectionEquality() .equals(other._unfoldSet, _unfoldSet) && + (identical(other.overrideData, overrideData) || + other.overrideData == overrideData) && (identical(other.isUpdating, isUpdating) || other.isUpdating == isUpdating)); } @@ -566,6 +598,7 @@ class _$ProfileImpl implements _Profile { autoUpdate, const DeepCollectionEquality().hash(_selectedMap), const DeepCollectionEquality().hash(_unfoldSet), + overrideData, isUpdating); /// Create a copy of Profile @@ -596,6 +629,7 @@ abstract class _Profile implements Profile { final bool autoUpdate, final Map selectedMap, final Set unfoldSet, + final OverrideData overrideData, @JsonKey(includeToJson: false, includeFromJson: false) final bool isUpdating}) = _$ProfileImpl; @@ -622,6 +656,8 @@ abstract class _Profile implements Profile { @override Set get unfoldSet; @override + OverrideData get overrideData; + @override @JsonKey(includeToJson: false, includeFromJson: false) bool get isUpdating; @@ -632,3 +668,398 @@ abstract class _Profile implements Profile { _$$ProfileImplCopyWith<_$ProfileImpl> get copyWith => throw _privateConstructorUsedError; } + +OverrideData _$OverrideDataFromJson(Map json) { + return _OverrideData.fromJson(json); +} + +/// @nodoc +mixin _$OverrideData { + bool get enable => throw _privateConstructorUsedError; + OverrideRule get rule => throw _privateConstructorUsedError; + + /// Serializes this OverrideData to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of OverrideData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OverrideDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OverrideDataCopyWith<$Res> { + factory $OverrideDataCopyWith( + OverrideData value, $Res Function(OverrideData) then) = + _$OverrideDataCopyWithImpl<$Res, OverrideData>; + @useResult + $Res call({bool enable, OverrideRule rule}); + + $OverrideRuleCopyWith<$Res> get rule; +} + +/// @nodoc +class _$OverrideDataCopyWithImpl<$Res, $Val extends OverrideData> + implements $OverrideDataCopyWith<$Res> { + _$OverrideDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OverrideData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enable = null, + Object? rule = null, + }) { + return _then(_value.copyWith( + enable: null == enable + ? _value.enable + : enable // ignore: cast_nullable_to_non_nullable + as bool, + rule: null == rule + ? _value.rule + : rule // ignore: cast_nullable_to_non_nullable + as OverrideRule, + ) as $Val); + } + + /// Create a copy of OverrideData + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OverrideRuleCopyWith<$Res> get rule { + return $OverrideRuleCopyWith<$Res>(_value.rule, (value) { + return _then(_value.copyWith(rule: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$OverrideDataImplCopyWith<$Res> + implements $OverrideDataCopyWith<$Res> { + factory _$$OverrideDataImplCopyWith( + _$OverrideDataImpl value, $Res Function(_$OverrideDataImpl) then) = + __$$OverrideDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool enable, OverrideRule rule}); + + @override + $OverrideRuleCopyWith<$Res> get rule; +} + +/// @nodoc +class __$$OverrideDataImplCopyWithImpl<$Res> + extends _$OverrideDataCopyWithImpl<$Res, _$OverrideDataImpl> + implements _$$OverrideDataImplCopyWith<$Res> { + __$$OverrideDataImplCopyWithImpl( + _$OverrideDataImpl _value, $Res Function(_$OverrideDataImpl) _then) + : super(_value, _then); + + /// Create a copy of OverrideData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enable = null, + Object? rule = null, + }) { + return _then(_$OverrideDataImpl( + enable: null == enable + ? _value.enable + : enable // ignore: cast_nullable_to_non_nullable + as bool, + rule: null == rule + ? _value.rule + : rule // ignore: cast_nullable_to_non_nullable + as OverrideRule, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$OverrideDataImpl implements _OverrideData { + const _$OverrideDataImpl( + {this.enable = false, this.rule = const OverrideRule()}); + + factory _$OverrideDataImpl.fromJson(Map json) => + _$$OverrideDataImplFromJson(json); + + @override + @JsonKey() + final bool enable; + @override + @JsonKey() + final OverrideRule rule; + + @override + String toString() { + return 'OverrideData(enable: $enable, rule: $rule)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OverrideDataImpl && + (identical(other.enable, enable) || other.enable == enable) && + (identical(other.rule, rule) || other.rule == rule)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, enable, rule); + + /// Create a copy of OverrideData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OverrideDataImplCopyWith<_$OverrideDataImpl> get copyWith => + __$$OverrideDataImplCopyWithImpl<_$OverrideDataImpl>(this, _$identity); + + @override + Map toJson() { + return _$$OverrideDataImplToJson( + this, + ); + } +} + +abstract class _OverrideData implements OverrideData { + const factory _OverrideData({final bool enable, final OverrideRule rule}) = + _$OverrideDataImpl; + + factory _OverrideData.fromJson(Map json) = + _$OverrideDataImpl.fromJson; + + @override + bool get enable; + @override + OverrideRule get rule; + + /// Create a copy of OverrideData + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OverrideDataImplCopyWith<_$OverrideDataImpl> get copyWith => + throw _privateConstructorUsedError; +} + +OverrideRule _$OverrideRuleFromJson(Map json) { + return _OverrideRule.fromJson(json); +} + +/// @nodoc +mixin _$OverrideRule { + OverrideRuleType get type => throw _privateConstructorUsedError; + List get overrideRules => throw _privateConstructorUsedError; + List get addedRules => throw _privateConstructorUsedError; + + /// Serializes this OverrideRule to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of OverrideRule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OverrideRuleCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OverrideRuleCopyWith<$Res> { + factory $OverrideRuleCopyWith( + OverrideRule value, $Res Function(OverrideRule) then) = + _$OverrideRuleCopyWithImpl<$Res, OverrideRule>; + @useResult + $Res call( + {OverrideRuleType type, List overrideRules, List addedRules}); +} + +/// @nodoc +class _$OverrideRuleCopyWithImpl<$Res, $Val extends OverrideRule> + implements $OverrideRuleCopyWith<$Res> { + _$OverrideRuleCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OverrideRule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? overrideRules = null, + Object? addedRules = null, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as OverrideRuleType, + overrideRules: null == overrideRules + ? _value.overrideRules + : overrideRules // ignore: cast_nullable_to_non_nullable + as List, + addedRules: null == addedRules + ? _value.addedRules + : addedRules // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$OverrideRuleImplCopyWith<$Res> + implements $OverrideRuleCopyWith<$Res> { + factory _$$OverrideRuleImplCopyWith( + _$OverrideRuleImpl value, $Res Function(_$OverrideRuleImpl) then) = + __$$OverrideRuleImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {OverrideRuleType type, List overrideRules, List addedRules}); +} + +/// @nodoc +class __$$OverrideRuleImplCopyWithImpl<$Res> + extends _$OverrideRuleCopyWithImpl<$Res, _$OverrideRuleImpl> + implements _$$OverrideRuleImplCopyWith<$Res> { + __$$OverrideRuleImplCopyWithImpl( + _$OverrideRuleImpl _value, $Res Function(_$OverrideRuleImpl) _then) + : super(_value, _then); + + /// Create a copy of OverrideRule + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? overrideRules = null, + Object? addedRules = null, + }) { + return _then(_$OverrideRuleImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as OverrideRuleType, + overrideRules: null == overrideRules + ? _value._overrideRules + : overrideRules // ignore: cast_nullable_to_non_nullable + as List, + addedRules: null == addedRules + ? _value._addedRules + : addedRules // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$OverrideRuleImpl implements _OverrideRule { + const _$OverrideRuleImpl( + {this.type = OverrideRuleType.added, + final List overrideRules = const [], + final List addedRules = const []}) + : _overrideRules = overrideRules, + _addedRules = addedRules; + + factory _$OverrideRuleImpl.fromJson(Map json) => + _$$OverrideRuleImplFromJson(json); + + @override + @JsonKey() + final OverrideRuleType type; + final List _overrideRules; + @override + @JsonKey() + List get overrideRules { + if (_overrideRules is EqualUnmodifiableListView) return _overrideRules; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_overrideRules); + } + + final List _addedRules; + @override + @JsonKey() + List get addedRules { + if (_addedRules is EqualUnmodifiableListView) return _addedRules; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_addedRules); + } + + @override + String toString() { + return 'OverrideRule(type: $type, overrideRules: $overrideRules, addedRules: $addedRules)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OverrideRuleImpl && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality() + .equals(other._overrideRules, _overrideRules) && + const DeepCollectionEquality() + .equals(other._addedRules, _addedRules)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + type, + const DeepCollectionEquality().hash(_overrideRules), + const DeepCollectionEquality().hash(_addedRules)); + + /// Create a copy of OverrideRule + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OverrideRuleImplCopyWith<_$OverrideRuleImpl> get copyWith => + __$$OverrideRuleImplCopyWithImpl<_$OverrideRuleImpl>(this, _$identity); + + @override + Map toJson() { + return _$$OverrideRuleImplToJson( + this, + ); + } +} + +abstract class _OverrideRule implements OverrideRule { + const factory _OverrideRule( + {final OverrideRuleType type, + final List overrideRules, + final List addedRules}) = _$OverrideRuleImpl; + + factory _OverrideRule.fromJson(Map json) = + _$OverrideRuleImpl.fromJson; + + @override + OverrideRuleType get type; + @override + List get overrideRules; + @override + List get addedRules; + + /// Create a copy of OverrideRule + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OverrideRuleImplCopyWith<_$OverrideRuleImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/profile.g.dart b/lib/models/generated/profile.g.dart index ac8f926..7b76002 100644 --- a/lib/models/generated/profile.g.dart +++ b/lib/models/generated/profile.g.dart @@ -48,6 +48,9 @@ _$ProfileImpl _$$ProfileImplFromJson(Map json) => ?.map((e) => e as String) .toSet() ?? const {}, + overrideData: json['overrideData'] == null + ? const OverrideData() + : OverrideData.fromJson(json['overrideData'] as Map), ); Map _$$ProfileImplToJson(_$ProfileImpl instance) => @@ -62,4 +65,45 @@ Map _$$ProfileImplToJson(_$ProfileImpl instance) => 'autoUpdate': instance.autoUpdate, 'selectedMap': instance.selectedMap, 'unfoldSet': instance.unfoldSet.toList(), + 'overrideData': instance.overrideData, }; + +_$OverrideDataImpl _$$OverrideDataImplFromJson(Map json) => + _$OverrideDataImpl( + enable: json['enable'] as bool? ?? false, + rule: json['rule'] == null + ? const OverrideRule() + : OverrideRule.fromJson(json['rule'] as Map), + ); + +Map _$$OverrideDataImplToJson(_$OverrideDataImpl instance) => + { + 'enable': instance.enable, + 'rule': instance.rule, + }; + +_$OverrideRuleImpl _$$OverrideRuleImplFromJson(Map json) => + _$OverrideRuleImpl( + type: $enumDecodeNullable(_$OverrideRuleTypeEnumMap, json['type']) ?? + OverrideRuleType.added, + overrideRules: (json['overrideRules'] as List?) + ?.map((e) => Rule.fromJson(e as Map)) + .toList() ?? + const [], + addedRules: (json['addedRules'] as List?) + ?.map((e) => Rule.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$$OverrideRuleImplToJson(_$OverrideRuleImpl instance) => + { + 'type': _$OverrideRuleTypeEnumMap[instance.type]!, + 'overrideRules': instance.overrideRules, + 'addedRules': instance.addedRules, + }; + +const _$OverrideRuleTypeEnumMap = { + OverrideRuleType.override: 'override', + OverrideRuleType.added: 'added', +}; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 8346e0e..9aff17d 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -161,6 +161,172 @@ abstract class _VM2 implements VM2 { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$VM3 { + A get a => throw _privateConstructorUsedError; + B get b => throw _privateConstructorUsedError; + C get c => throw _privateConstructorUsedError; + + /// Create a copy of VM3 + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VM3CopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VM3CopyWith { + factory $VM3CopyWith(VM3 value, $Res Function(VM3) then) = + _$VM3CopyWithImpl>; + @useResult + $Res call({A a, B b, C c}); +} + +/// @nodoc +class _$VM3CopyWithImpl> + implements $VM3CopyWith { + _$VM3CopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VM3 + /// 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, + }) { + 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, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$VM3ImplCopyWith + implements $VM3CopyWith { + factory _$$VM3ImplCopyWith( + _$VM3Impl value, $Res Function(_$VM3Impl) then) = + __$$VM3ImplCopyWithImpl; + @override + @useResult + $Res call({A a, B b, C c}); +} + +/// @nodoc +class __$$VM3ImplCopyWithImpl + extends _$VM3CopyWithImpl> + implements _$$VM3ImplCopyWith { + __$$VM3ImplCopyWithImpl( + _$VM3Impl _value, $Res Function(_$VM3Impl) _then) + : super(_value, _then); + + /// Create a copy of VM3 + /// 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, + }) { + return _then(_$VM3Impl( + 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, + )); + } +} + +/// @nodoc + +class _$VM3Impl implements _VM3 { + const _$VM3Impl({required this.a, required this.b, required this.c}); + + @override + final A a; + @override + final B b; + @override + final C c; + + @override + String toString() { + return 'VM3<$A, $B, $C>(a: $a, b: $b, c: $c)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VM3Impl && + const DeepCollectionEquality().equals(other.a, a) && + const DeepCollectionEquality().equals(other.b, b) && + const DeepCollectionEquality().equals(other.c, c)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(a), + const DeepCollectionEquality().hash(b), + const DeepCollectionEquality().hash(c)); + + /// Create a copy of VM3 + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VM3ImplCopyWith> get copyWith => + __$$VM3ImplCopyWithImpl>(this, _$identity); +} + +abstract class _VM3 implements VM3 { + const factory _VM3( + {required final A a, + required final B b, + required final C c}) = _$VM3Impl; + + @override + A get a; + @override + B get b; + @override + C get c; + + /// Create a copy of VM3 + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VM3ImplCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$StartButtonSelectorState { bool get isInit => throw _privateConstructorUsedError; @@ -498,6 +664,7 @@ abstract class _ProfilesSelectorState implements ProfilesSelectorState { /// @nodoc mixin _$NetworkDetectionState { + bool get isLoading => throw _privateConstructorUsedError; bool get isTesting => throw _privateConstructorUsedError; IpInfo? get ipInfo => throw _privateConstructorUsedError; @@ -514,7 +681,7 @@ abstract class $NetworkDetectionStateCopyWith<$Res> { $Res Function(NetworkDetectionState) then) = _$NetworkDetectionStateCopyWithImpl<$Res, NetworkDetectionState>; @useResult - $Res call({bool isTesting, IpInfo? ipInfo}); + $Res call({bool isLoading, bool isTesting, IpInfo? ipInfo}); } /// @nodoc @@ -533,10 +700,15 @@ class _$NetworkDetectionStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ + Object? isLoading = null, Object? isTesting = null, Object? ipInfo = freezed, }) { return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, isTesting: null == isTesting ? _value.isTesting : isTesting // ignore: cast_nullable_to_non_nullable @@ -558,7 +730,7 @@ abstract class _$$NetworkDetectionStateImplCopyWith<$Res> __$$NetworkDetectionStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isTesting, IpInfo? ipInfo}); + $Res call({bool isLoading, bool isTesting, IpInfo? ipInfo}); } /// @nodoc @@ -575,10 +747,15 @@ class __$$NetworkDetectionStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? isLoading = null, Object? isTesting = null, Object? ipInfo = freezed, }) { return _then(_$NetworkDetectionStateImpl( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, isTesting: null == isTesting ? _value.isTesting : isTesting // ignore: cast_nullable_to_non_nullable @@ -595,8 +772,10 @@ class __$$NetworkDetectionStateImplCopyWithImpl<$Res> class _$NetworkDetectionStateImpl implements _NetworkDetectionState { const _$NetworkDetectionStateImpl( - {required this.isTesting, required this.ipInfo}); + {required this.isLoading, required this.isTesting, required this.ipInfo}); + @override + final bool isLoading; @override final bool isTesting; @override @@ -604,7 +783,7 @@ class _$NetworkDetectionStateImpl implements _NetworkDetectionState { @override String toString() { - return 'NetworkDetectionState(isTesting: $isTesting, ipInfo: $ipInfo)'; + return 'NetworkDetectionState(isLoading: $isLoading, isTesting: $isTesting, ipInfo: $ipInfo)'; } @override @@ -612,13 +791,15 @@ class _$NetworkDetectionStateImpl implements _NetworkDetectionState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$NetworkDetectionStateImpl && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && (identical(other.isTesting, isTesting) || other.isTesting == isTesting) && (identical(other.ipInfo, ipInfo) || other.ipInfo == ipInfo)); } @override - int get hashCode => Object.hash(runtimeType, isTesting, ipInfo); + int get hashCode => Object.hash(runtimeType, isLoading, isTesting, ipInfo); /// Create a copy of NetworkDetectionState /// with the given fields replaced by the non-null parameter values. @@ -632,9 +813,12 @@ class _$NetworkDetectionStateImpl implements _NetworkDetectionState { abstract class _NetworkDetectionState implements NetworkDetectionState { const factory _NetworkDetectionState( - {required final bool isTesting, + {required final bool isLoading, + required final bool isTesting, required final IpInfo? ipInfo}) = _$NetworkDetectionStateImpl; + @override + bool get isLoading; @override bool get isTesting; @override @@ -3150,6 +3334,7 @@ abstract class _ProxyState implements ProxyState { mixin _$ClashConfigState { bool get overrideDns => throw _privateConstructorUsedError; ClashConfig get clashConfig => throw _privateConstructorUsedError; + OverrideData get overrideData => throw _privateConstructorUsedError; /// Create a copy of ClashConfigState /// with the given fields replaced by the non-null parameter values. @@ -3164,9 +3349,11 @@ abstract class $ClashConfigStateCopyWith<$Res> { ClashConfigState value, $Res Function(ClashConfigState) then) = _$ClashConfigStateCopyWithImpl<$Res, ClashConfigState>; @useResult - $Res call({bool overrideDns, ClashConfig clashConfig}); + $Res call( + {bool overrideDns, ClashConfig clashConfig, OverrideData overrideData}); $ClashConfigCopyWith<$Res> get clashConfig; + $OverrideDataCopyWith<$Res> get overrideData; } /// @nodoc @@ -3186,6 +3373,7 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> $Res call({ Object? overrideDns = null, Object? clashConfig = null, + Object? overrideData = null, }) { return _then(_value.copyWith( overrideDns: null == overrideDns @@ -3196,6 +3384,10 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> ? _value.clashConfig : clashConfig // ignore: cast_nullable_to_non_nullable as ClashConfig, + overrideData: null == overrideData + ? _value.overrideData + : overrideData // ignore: cast_nullable_to_non_nullable + as OverrideData, ) as $Val); } @@ -3208,6 +3400,16 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> return _then(_value.copyWith(clashConfig: value) as $Val); }); } + + /// Create a copy of ClashConfigState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OverrideDataCopyWith<$Res> get overrideData { + return $OverrideDataCopyWith<$Res>(_value.overrideData, (value) { + return _then(_value.copyWith(overrideData: value) as $Val); + }); + } } /// @nodoc @@ -3218,10 +3420,13 @@ abstract class _$$ClashConfigStateImplCopyWith<$Res> __$$ClashConfigStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool overrideDns, ClashConfig clashConfig}); + $Res call( + {bool overrideDns, ClashConfig clashConfig, OverrideData overrideData}); @override $ClashConfigCopyWith<$Res> get clashConfig; + @override + $OverrideDataCopyWith<$Res> get overrideData; } /// @nodoc @@ -3239,6 +3444,7 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> $Res call({ Object? overrideDns = null, Object? clashConfig = null, + Object? overrideData = null, }) { return _then(_$ClashConfigStateImpl( overrideDns: null == overrideDns @@ -3249,6 +3455,10 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> ? _value.clashConfig : clashConfig // ignore: cast_nullable_to_non_nullable as ClashConfig, + overrideData: null == overrideData + ? _value.overrideData + : overrideData // ignore: cast_nullable_to_non_nullable + as OverrideData, )); } } @@ -3257,16 +3467,20 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> class _$ClashConfigStateImpl implements _ClashConfigState { const _$ClashConfigStateImpl( - {required this.overrideDns, required this.clashConfig}); + {required this.overrideDns, + required this.clashConfig, + required this.overrideData}); @override final bool overrideDns; @override final ClashConfig clashConfig; + @override + final OverrideData overrideData; @override String toString() { - return 'ClashConfigState(overrideDns: $overrideDns, clashConfig: $clashConfig)'; + return 'ClashConfigState(overrideDns: $overrideDns, clashConfig: $clashConfig, overrideData: $overrideData)'; } @override @@ -3277,11 +3491,14 @@ class _$ClashConfigStateImpl implements _ClashConfigState { (identical(other.overrideDns, overrideDns) || other.overrideDns == overrideDns) && (identical(other.clashConfig, clashConfig) || - other.clashConfig == clashConfig)); + other.clashConfig == clashConfig) && + (identical(other.overrideData, overrideData) || + other.overrideData == overrideData)); } @override - int get hashCode => Object.hash(runtimeType, overrideDns, clashConfig); + int get hashCode => + Object.hash(runtimeType, overrideDns, clashConfig, overrideData); /// Create a copy of ClashConfigState /// with the given fields replaced by the non-null parameter values. @@ -3296,12 +3513,15 @@ class _$ClashConfigStateImpl implements _ClashConfigState { abstract class _ClashConfigState implements ClashConfigState { const factory _ClashConfigState( {required final bool overrideDns, - required final ClashConfig clashConfig}) = _$ClashConfigStateImpl; + required final ClashConfig clashConfig, + required final OverrideData overrideData}) = _$ClashConfigStateImpl; @override bool get overrideDns; @override ClashConfig get clashConfig; + @override + OverrideData get overrideData; /// Create a copy of ClashConfigState /// with the given fields replaced by the non-null parameter values. @@ -3780,3 +4000,246 @@ abstract class _VpnState implements VpnState { _$$VpnStateImplCopyWith<_$VpnStateImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$ProfileOverrideStateModel { + ClashConfigSnippet? get snippet => throw _privateConstructorUsedError; + bool get isEdit => throw _privateConstructorUsedError; + Set get selectedRules => throw _privateConstructorUsedError; + OverrideData? get overrideData => throw _privateConstructorUsedError; + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProfileOverrideStateModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProfileOverrideStateModelCopyWith<$Res> { + factory $ProfileOverrideStateModelCopyWith(ProfileOverrideStateModel value, + $Res Function(ProfileOverrideStateModel) then) = + _$ProfileOverrideStateModelCopyWithImpl<$Res, ProfileOverrideStateModel>; + @useResult + $Res call( + {ClashConfigSnippet? snippet, + bool isEdit, + Set selectedRules, + OverrideData? overrideData}); + + $ClashConfigSnippetCopyWith<$Res>? get snippet; + $OverrideDataCopyWith<$Res>? get overrideData; +} + +/// @nodoc +class _$ProfileOverrideStateModelCopyWithImpl<$Res, + $Val extends ProfileOverrideStateModel> + implements $ProfileOverrideStateModelCopyWith<$Res> { + _$ProfileOverrideStateModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? snippet = freezed, + Object? isEdit = null, + Object? selectedRules = null, + Object? overrideData = freezed, + }) { + return _then(_value.copyWith( + snippet: freezed == snippet + ? _value.snippet + : snippet // ignore: cast_nullable_to_non_nullable + as ClashConfigSnippet?, + isEdit: null == isEdit + ? _value.isEdit + : isEdit // ignore: cast_nullable_to_non_nullable + as bool, + selectedRules: null == selectedRules + ? _value.selectedRules + : selectedRules // ignore: cast_nullable_to_non_nullable + as Set, + overrideData: freezed == overrideData + ? _value.overrideData + : overrideData // ignore: cast_nullable_to_non_nullable + as OverrideData?, + ) as $Val); + } + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ClashConfigSnippetCopyWith<$Res>? get snippet { + if (_value.snippet == null) { + return null; + } + + return $ClashConfigSnippetCopyWith<$Res>(_value.snippet!, (value) { + return _then(_value.copyWith(snippet: value) as $Val); + }); + } + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OverrideDataCopyWith<$Res>? get overrideData { + if (_value.overrideData == null) { + return null; + } + + return $OverrideDataCopyWith<$Res>(_value.overrideData!, (value) { + return _then(_value.copyWith(overrideData: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ProfileOverrideStateModelImplCopyWith<$Res> + implements $ProfileOverrideStateModelCopyWith<$Res> { + factory _$$ProfileOverrideStateModelImplCopyWith( + _$ProfileOverrideStateModelImpl value, + $Res Function(_$ProfileOverrideStateModelImpl) then) = + __$$ProfileOverrideStateModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ClashConfigSnippet? snippet, + bool isEdit, + Set selectedRules, + OverrideData? overrideData}); + + @override + $ClashConfigSnippetCopyWith<$Res>? get snippet; + @override + $OverrideDataCopyWith<$Res>? get overrideData; +} + +/// @nodoc +class __$$ProfileOverrideStateModelImplCopyWithImpl<$Res> + extends _$ProfileOverrideStateModelCopyWithImpl<$Res, + _$ProfileOverrideStateModelImpl> + implements _$$ProfileOverrideStateModelImplCopyWith<$Res> { + __$$ProfileOverrideStateModelImplCopyWithImpl( + _$ProfileOverrideStateModelImpl _value, + $Res Function(_$ProfileOverrideStateModelImpl) _then) + : super(_value, _then); + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? snippet = freezed, + Object? isEdit = null, + Object? selectedRules = null, + Object? overrideData = freezed, + }) { + return _then(_$ProfileOverrideStateModelImpl( + snippet: freezed == snippet + ? _value.snippet + : snippet // ignore: cast_nullable_to_non_nullable + as ClashConfigSnippet?, + isEdit: null == isEdit + ? _value.isEdit + : isEdit // ignore: cast_nullable_to_non_nullable + as bool, + selectedRules: null == selectedRules + ? _value._selectedRules + : selectedRules // ignore: cast_nullable_to_non_nullable + as Set, + overrideData: freezed == overrideData + ? _value.overrideData + : overrideData // ignore: cast_nullable_to_non_nullable + as OverrideData?, + )); + } +} + +/// @nodoc + +class _$ProfileOverrideStateModelImpl implements _ProfileOverrideStateModel { + const _$ProfileOverrideStateModelImpl( + {this.snippet, + required this.isEdit, + required final Set selectedRules, + this.overrideData}) + : _selectedRules = selectedRules; + + @override + final ClashConfigSnippet? snippet; + @override + final bool isEdit; + final Set _selectedRules; + @override + Set get selectedRules { + if (_selectedRules is EqualUnmodifiableSetView) return _selectedRules; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_selectedRules); + } + + @override + final OverrideData? overrideData; + + @override + String toString() { + return 'ProfileOverrideStateModel(snippet: $snippet, isEdit: $isEdit, selectedRules: $selectedRules, overrideData: $overrideData)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProfileOverrideStateModelImpl && + (identical(other.snippet, snippet) || other.snippet == snippet) && + (identical(other.isEdit, isEdit) || other.isEdit == isEdit) && + const DeepCollectionEquality() + .equals(other._selectedRules, _selectedRules) && + (identical(other.overrideData, overrideData) || + other.overrideData == overrideData)); + } + + @override + int get hashCode => Object.hash(runtimeType, snippet, isEdit, + const DeepCollectionEquality().hash(_selectedRules), overrideData); + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProfileOverrideStateModelImplCopyWith<_$ProfileOverrideStateModelImpl> + get copyWith => __$$ProfileOverrideStateModelImplCopyWithImpl< + _$ProfileOverrideStateModelImpl>(this, _$identity); +} + +abstract class _ProfileOverrideStateModel implements ProfileOverrideStateModel { + const factory _ProfileOverrideStateModel( + {final ClashConfigSnippet? snippet, + required final bool isEdit, + required final Set selectedRules, + final OverrideData? overrideData}) = _$ProfileOverrideStateModelImpl; + + @override + ClashConfigSnippet? get snippet; + @override + bool get isEdit; + @override + Set get selectedRules; + @override + OverrideData? get overrideData; + + /// Create a copy of ProfileOverrideStateModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProfileOverrideStateModelImplCopyWith<_$ProfileOverrideStateModelImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/widget.freezed.dart b/lib/models/generated/widget.freezed.dart index e984f42..cc373eb 100644 --- a/lib/models/generated/widget.freezed.dart +++ b/lib/models/generated/widget.freezed.dart @@ -312,121 +312,155 @@ abstract class _CommonMessage implements CommonMessage { } /// @nodoc -mixin _$CommonAppBarState { +mixin _$AppBarState { List get actions => throw _privateConstructorUsedError; - dynamic Function(String)? get onSearch => throw _privateConstructorUsedError; - bool get searching => throw _privateConstructorUsedError; + AppBarSearchState? get searchState => throw _privateConstructorUsedError; + AppBarEditState? get editState => throw _privateConstructorUsedError; - /// Create a copy of CommonAppBarState + /// Create a copy of AppBarState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $CommonAppBarStateCopyWith get copyWith => + $AppBarStateCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $CommonAppBarStateCopyWith<$Res> { - factory $CommonAppBarStateCopyWith( - CommonAppBarState value, $Res Function(CommonAppBarState) then) = - _$CommonAppBarStateCopyWithImpl<$Res, CommonAppBarState>; +abstract class $AppBarStateCopyWith<$Res> { + factory $AppBarStateCopyWith( + AppBarState value, $Res Function(AppBarState) then) = + _$AppBarStateCopyWithImpl<$Res, AppBarState>; @useResult $Res call( {List actions, - dynamic Function(String)? onSearch, - bool searching}); + AppBarSearchState? searchState, + AppBarEditState? editState}); + + $AppBarSearchStateCopyWith<$Res>? get searchState; + $AppBarEditStateCopyWith<$Res>? get editState; } /// @nodoc -class _$CommonAppBarStateCopyWithImpl<$Res, $Val extends CommonAppBarState> - implements $CommonAppBarStateCopyWith<$Res> { - _$CommonAppBarStateCopyWithImpl(this._value, this._then); +class _$AppBarStateCopyWithImpl<$Res, $Val extends AppBarState> + implements $AppBarStateCopyWith<$Res> { + _$AppBarStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of CommonAppBarState + /// Create a copy of AppBarState /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? actions = null, - Object? onSearch = freezed, - Object? searching = null, + Object? searchState = freezed, + Object? editState = freezed, }) { return _then(_value.copyWith( actions: null == actions ? _value.actions : actions // ignore: cast_nullable_to_non_nullable as List, - onSearch: freezed == onSearch - ? _value.onSearch - : onSearch // ignore: cast_nullable_to_non_nullable - as dynamic Function(String)?, - searching: null == searching - ? _value.searching - : searching // ignore: cast_nullable_to_non_nullable - as bool, + searchState: freezed == searchState + ? _value.searchState + : searchState // ignore: cast_nullable_to_non_nullable + as AppBarSearchState?, + editState: freezed == editState + ? _value.editState + : editState // ignore: cast_nullable_to_non_nullable + as AppBarEditState?, ) as $Val); } + + /// Create a copy of AppBarState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AppBarSearchStateCopyWith<$Res>? get searchState { + if (_value.searchState == null) { + return null; + } + + return $AppBarSearchStateCopyWith<$Res>(_value.searchState!, (value) { + return _then(_value.copyWith(searchState: value) as $Val); + }); + } + + /// Create a copy of AppBarState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AppBarEditStateCopyWith<$Res>? get editState { + if (_value.editState == null) { + return null; + } + + return $AppBarEditStateCopyWith<$Res>(_value.editState!, (value) { + return _then(_value.copyWith(editState: value) as $Val); + }); + } } /// @nodoc -abstract class _$$CommonAppBarStateImplCopyWith<$Res> - implements $CommonAppBarStateCopyWith<$Res> { - factory _$$CommonAppBarStateImplCopyWith(_$CommonAppBarStateImpl value, - $Res Function(_$CommonAppBarStateImpl) then) = - __$$CommonAppBarStateImplCopyWithImpl<$Res>; +abstract class _$$AppBarStateImplCopyWith<$Res> + implements $AppBarStateCopyWith<$Res> { + factory _$$AppBarStateImplCopyWith( + _$AppBarStateImpl value, $Res Function(_$AppBarStateImpl) then) = + __$$AppBarStateImplCopyWithImpl<$Res>; @override @useResult $Res call( {List actions, - dynamic Function(String)? onSearch, - bool searching}); + AppBarSearchState? searchState, + AppBarEditState? editState}); + + @override + $AppBarSearchStateCopyWith<$Res>? get searchState; + @override + $AppBarEditStateCopyWith<$Res>? get editState; } /// @nodoc -class __$$CommonAppBarStateImplCopyWithImpl<$Res> - extends _$CommonAppBarStateCopyWithImpl<$Res, _$CommonAppBarStateImpl> - implements _$$CommonAppBarStateImplCopyWith<$Res> { - __$$CommonAppBarStateImplCopyWithImpl(_$CommonAppBarStateImpl _value, - $Res Function(_$CommonAppBarStateImpl) _then) +class __$$AppBarStateImplCopyWithImpl<$Res> + extends _$AppBarStateCopyWithImpl<$Res, _$AppBarStateImpl> + implements _$$AppBarStateImplCopyWith<$Res> { + __$$AppBarStateImplCopyWithImpl( + _$AppBarStateImpl _value, $Res Function(_$AppBarStateImpl) _then) : super(_value, _then); - /// Create a copy of CommonAppBarState + /// Create a copy of AppBarState /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? actions = null, - Object? onSearch = freezed, - Object? searching = null, + Object? searchState = freezed, + Object? editState = freezed, }) { - return _then(_$CommonAppBarStateImpl( + return _then(_$AppBarStateImpl( actions: null == actions ? _value._actions : actions // ignore: cast_nullable_to_non_nullable as List, - onSearch: freezed == onSearch - ? _value.onSearch - : onSearch // ignore: cast_nullable_to_non_nullable - as dynamic Function(String)?, - searching: null == searching - ? _value.searching - : searching // ignore: cast_nullable_to_non_nullable - as bool, + searchState: freezed == searchState + ? _value.searchState + : searchState // ignore: cast_nullable_to_non_nullable + as AppBarSearchState?, + editState: freezed == editState + ? _value.editState + : editState // ignore: cast_nullable_to_non_nullable + as AppBarEditState?, )); } } /// @nodoc -class _$CommonAppBarStateImpl implements _CommonAppBarState { - const _$CommonAppBarStateImpl( - {final List actions = const [], - this.onSearch, - this.searching = false}) +class _$AppBarStateImpl implements _AppBarState { + const _$AppBarStateImpl( + {final List actions = const [], this.searchState, this.editState}) : _actions = actions; final List _actions; @@ -439,59 +473,373 @@ class _$CommonAppBarStateImpl implements _CommonAppBarState { } @override - final dynamic Function(String)? onSearch; + final AppBarSearchState? searchState; @override - @JsonKey() - final bool searching; + final AppBarEditState? editState; @override String toString() { - return 'CommonAppBarState(actions: $actions, onSearch: $onSearch, searching: $searching)'; + return 'AppBarState(actions: $actions, searchState: $searchState, editState: $editState)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$CommonAppBarStateImpl && + other is _$AppBarStateImpl && const DeepCollectionEquality().equals(other._actions, _actions) && - (identical(other.onSearch, onSearch) || - other.onSearch == onSearch) && - (identical(other.searching, searching) || - other.searching == searching)); + (identical(other.searchState, searchState) || + other.searchState == searchState) && + (identical(other.editState, editState) || + other.editState == editState)); } @override int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(_actions), onSearch, searching); + const DeepCollectionEquality().hash(_actions), searchState, editState); - /// Create a copy of CommonAppBarState + /// Create a copy of AppBarState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') - _$$CommonAppBarStateImplCopyWith<_$CommonAppBarStateImpl> get copyWith => - __$$CommonAppBarStateImplCopyWithImpl<_$CommonAppBarStateImpl>( - this, _$identity); + _$$AppBarStateImplCopyWith<_$AppBarStateImpl> get copyWith => + __$$AppBarStateImplCopyWithImpl<_$AppBarStateImpl>(this, _$identity); } -abstract class _CommonAppBarState implements CommonAppBarState { - const factory _CommonAppBarState( +abstract class _AppBarState implements AppBarState { + const factory _AppBarState( {final List actions, - final dynamic Function(String)? onSearch, - final bool searching}) = _$CommonAppBarStateImpl; + final AppBarSearchState? searchState, + final AppBarEditState? editState}) = _$AppBarStateImpl; @override List get actions; @override - dynamic Function(String)? get onSearch; + AppBarSearchState? get searchState; @override - bool get searching; + AppBarEditState? get editState; - /// Create a copy of CommonAppBarState + /// Create a copy of AppBarState /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) - _$$CommonAppBarStateImplCopyWith<_$CommonAppBarStateImpl> get copyWith => + _$$AppBarStateImplCopyWith<_$AppBarStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$AppBarSearchState { + dynamic Function(String) get onSearch => throw _privateConstructorUsedError; + bool get isSearch => throw _privateConstructorUsedError; + + /// Create a copy of AppBarSearchState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AppBarSearchStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AppBarSearchStateCopyWith<$Res> { + factory $AppBarSearchStateCopyWith( + AppBarSearchState value, $Res Function(AppBarSearchState) then) = + _$AppBarSearchStateCopyWithImpl<$Res, AppBarSearchState>; + @useResult + $Res call({dynamic Function(String) onSearch, bool isSearch}); +} + +/// @nodoc +class _$AppBarSearchStateCopyWithImpl<$Res, $Val extends AppBarSearchState> + implements $AppBarSearchStateCopyWith<$Res> { + _$AppBarSearchStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AppBarSearchState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? onSearch = null, + Object? isSearch = null, + }) { + return _then(_value.copyWith( + onSearch: null == onSearch + ? _value.onSearch + : onSearch // ignore: cast_nullable_to_non_nullable + as dynamic Function(String), + isSearch: null == isSearch + ? _value.isSearch + : isSearch // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AppBarSearchStateImplCopyWith<$Res> + implements $AppBarSearchStateCopyWith<$Res> { + factory _$$AppBarSearchStateImplCopyWith(_$AppBarSearchStateImpl value, + $Res Function(_$AppBarSearchStateImpl) then) = + __$$AppBarSearchStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({dynamic Function(String) onSearch, bool isSearch}); +} + +/// @nodoc +class __$$AppBarSearchStateImplCopyWithImpl<$Res> + extends _$AppBarSearchStateCopyWithImpl<$Res, _$AppBarSearchStateImpl> + implements _$$AppBarSearchStateImplCopyWith<$Res> { + __$$AppBarSearchStateImplCopyWithImpl(_$AppBarSearchStateImpl _value, + $Res Function(_$AppBarSearchStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AppBarSearchState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? onSearch = null, + Object? isSearch = null, + }) { + return _then(_$AppBarSearchStateImpl( + onSearch: null == onSearch + ? _value.onSearch + : onSearch // ignore: cast_nullable_to_non_nullable + as dynamic Function(String), + isSearch: null == isSearch + ? _value.isSearch + : isSearch // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$AppBarSearchStateImpl implements _AppBarSearchState { + const _$AppBarSearchStateImpl( + {required this.onSearch, this.isSearch = false}); + + @override + final dynamic Function(String) onSearch; + @override + @JsonKey() + final bool isSearch; + + @override + String toString() { + return 'AppBarSearchState(onSearch: $onSearch, isSearch: $isSearch)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AppBarSearchStateImpl && + (identical(other.onSearch, onSearch) || + other.onSearch == onSearch) && + (identical(other.isSearch, isSearch) || + other.isSearch == isSearch)); + } + + @override + int get hashCode => Object.hash(runtimeType, onSearch, isSearch); + + /// Create a copy of AppBarSearchState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AppBarSearchStateImplCopyWith<_$AppBarSearchStateImpl> get copyWith => + __$$AppBarSearchStateImplCopyWithImpl<_$AppBarSearchStateImpl>( + this, _$identity); +} + +abstract class _AppBarSearchState implements AppBarSearchState { + const factory _AppBarSearchState( + {required final dynamic Function(String) onSearch, + final bool isSearch}) = _$AppBarSearchStateImpl; + + @override + dynamic Function(String) get onSearch; + @override + bool get isSearch; + + /// Create a copy of AppBarSearchState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AppBarSearchStateImplCopyWith<_$AppBarSearchStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$AppBarEditState { + dynamic get editCount => throw _privateConstructorUsedError; + bool get isEdit => throw _privateConstructorUsedError; + dynamic Function() get onExit => throw _privateConstructorUsedError; + + /// Create a copy of AppBarEditState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AppBarEditStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AppBarEditStateCopyWith<$Res> { + factory $AppBarEditStateCopyWith( + AppBarEditState value, $Res Function(AppBarEditState) then) = + _$AppBarEditStateCopyWithImpl<$Res, AppBarEditState>; + @useResult + $Res call({dynamic editCount, bool isEdit, dynamic Function() onExit}); +} + +/// @nodoc +class _$AppBarEditStateCopyWithImpl<$Res, $Val extends AppBarEditState> + implements $AppBarEditStateCopyWith<$Res> { + _$AppBarEditStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AppBarEditState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? editCount = freezed, + Object? isEdit = null, + Object? onExit = null, + }) { + return _then(_value.copyWith( + editCount: freezed == editCount + ? _value.editCount + : editCount // ignore: cast_nullable_to_non_nullable + as dynamic, + isEdit: null == isEdit + ? _value.isEdit + : isEdit // ignore: cast_nullable_to_non_nullable + as bool, + onExit: null == onExit + ? _value.onExit + : onExit // ignore: cast_nullable_to_non_nullable + as dynamic Function(), + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AppBarEditStateImplCopyWith<$Res> + implements $AppBarEditStateCopyWith<$Res> { + factory _$$AppBarEditStateImplCopyWith(_$AppBarEditStateImpl value, + $Res Function(_$AppBarEditStateImpl) then) = + __$$AppBarEditStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({dynamic editCount, bool isEdit, dynamic Function() onExit}); +} + +/// @nodoc +class __$$AppBarEditStateImplCopyWithImpl<$Res> + extends _$AppBarEditStateCopyWithImpl<$Res, _$AppBarEditStateImpl> + implements _$$AppBarEditStateImplCopyWith<$Res> { + __$$AppBarEditStateImplCopyWithImpl( + _$AppBarEditStateImpl _value, $Res Function(_$AppBarEditStateImpl) _then) + : super(_value, _then); + + /// Create a copy of AppBarEditState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? editCount = freezed, + Object? isEdit = null, + Object? onExit = null, + }) { + return _then(_$AppBarEditStateImpl( + editCount: freezed == editCount ? _value.editCount! : editCount, + isEdit: null == isEdit + ? _value.isEdit + : isEdit // ignore: cast_nullable_to_non_nullable + as bool, + onExit: null == onExit + ? _value.onExit + : onExit // ignore: cast_nullable_to_non_nullable + as dynamic Function(), + )); + } +} + +/// @nodoc + +class _$AppBarEditStateImpl implements _AppBarEditState { + const _$AppBarEditStateImpl( + {this.editCount = 0, this.isEdit = false, required this.onExit}); + + @override + @JsonKey() + final dynamic editCount; + @override + @JsonKey() + final bool isEdit; + @override + final dynamic Function() onExit; + + @override + String toString() { + return 'AppBarEditState(editCount: $editCount, isEdit: $isEdit, onExit: $onExit)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AppBarEditStateImpl && + const DeepCollectionEquality().equals(other.editCount, editCount) && + (identical(other.isEdit, isEdit) || other.isEdit == isEdit) && + (identical(other.onExit, onExit) || other.onExit == onExit)); + } + + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(editCount), isEdit, onExit); + + /// Create a copy of AppBarEditState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AppBarEditStateImplCopyWith<_$AppBarEditStateImpl> get copyWith => + __$$AppBarEditStateImplCopyWithImpl<_$AppBarEditStateImpl>( + this, _$identity); +} + +abstract class _AppBarEditState implements AppBarEditState { + const factory _AppBarEditState( + {final dynamic editCount, + final bool isEdit, + required final dynamic Function() onExit}) = _$AppBarEditStateImpl; + + @override + dynamic get editCount; + @override + bool get isEdit; + @override + dynamic Function() get onExit; + + /// Create a copy of AppBarEditState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AppBarEditStateImplCopyWith<_$AppBarEditStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 0653c42..99a9926 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -8,7 +8,10 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'clash_config.dart'; + part 'generated/profile.freezed.dart'; + part 'generated/profile.g.dart'; typedef SelectedMap = Map; @@ -55,6 +58,7 @@ class Profile with _$Profile { @Default(true) bool autoUpdate, @Default({}) SelectedMap selectedMap, @Default({}) Set unfoldSet, + @Default(OverrideData()) OverrideData overrideData, @JsonKey(includeToJson: false, includeFromJson: false) @Default(false) bool isUpdating, @@ -76,6 +80,51 @@ class Profile with _$Profile { } } +@freezed +class OverrideData with _$OverrideData { + const factory OverrideData({ + @Default(false) bool enable, + @Default(OverrideRule()) OverrideRule rule, + }) = _OverrideData; + + factory OverrideData.fromJson(Map json) => + _$OverrideDataFromJson(json); +} + +extension OverrideDataExt on OverrideData { + List get runningRule { + if (!enable) { + return []; + } + return rule.rules.map((item) => item.value).toList(); + } +} + +@freezed +class OverrideRule with _$OverrideRule { + const factory OverrideRule({ + @Default(OverrideRuleType.added) OverrideRuleType type, + @Default([]) List overrideRules, + @Default([]) List addedRules, + }) = _OverrideRule; + + factory OverrideRule.fromJson(Map json) => + _$OverrideRuleFromJson(json); +} + +extension OverrideRuleExt on OverrideRule { + List get rules => switch (type == OverrideRuleType.override) { + true => overrideRules, + false => addedRules, + }; + + OverrideRule updateRules(List Function(List rules) builder) { + if (type == OverrideRuleType.added) { + return copyWith(addedRules: builder(addedRules)); + } + return copyWith(overrideRules: builder(overrideRules)); + } +} extension ProfilesExt on List { Profile? getProfile(String? profileId) { diff --git a/lib/models/selector.dart b/lib/models/selector.dart index c45f60a..384d3b4 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -15,6 +15,14 @@ class VM2 with _$VM2 { }) = _VM2; } +@freezed +class VM3 with _$VM3 { + const factory VM3({ + required A a, + required B b, + required C c, + }) = _VM3; +} @freezed class StartButtonSelectorState with _$StartButtonSelectorState { @@ -36,6 +44,7 @@ class ProfilesSelectorState with _$ProfilesSelectorState { @freezed class NetworkDetectionState with _$NetworkDetectionState { const factory NetworkDetectionState({ + required bool isLoading, required bool isTesting, required IpInfo? ipInfo, }) = _NetworkDetectionState; @@ -143,19 +152,18 @@ extension PackageListSelectorStateExt on PackageListSelectorState { return packages .where((item) => isFilterSystemApp ? item.isSystem == false : true) .sorted( - (a, b) { + (a, b) { return switch (sort) { AccessSortType.none => 0, - AccessSortType.name => - other.sortByChar( - other.getPinyin(a.label), - other.getPinyin(b.label), - ), + AccessSortType.name => other.sortByChar( + other.getPinyin(a.label), + other.getPinyin(b.label), + ), AccessSortType.time => b.lastUpdateTime.compareTo(a.lastUpdateTime), }; }, ).sorted( - (a, b) { + (a, b) { final isSelectA = selectedList.contains(a.packageName); final isSelectB = selectedList.contains(b.packageName); if (isSelectA && isSelectB) return 0; @@ -199,6 +207,7 @@ class ClashConfigState with _$ClashConfigState { const factory ClashConfigState({ required bool overrideDns, required ClashConfig clashConfig, + required OverrideData overrideData, }) = _ClashConfigState; } @@ -225,3 +234,13 @@ class VpnState with _$VpnState { required VpnProps vpnProps, }) = _VpnState; } + +@freezed +class ProfileOverrideStateModel with _$ProfileOverrideStateModel { + const factory ProfileOverrideStateModel({ + ClashConfigSnippet? snippet, + required bool isEdit, + required Set selectedRules, + OverrideData? overrideData, + }) = _ProfileOverrideStateModel; +} \ No newline at end of file diff --git a/lib/models/widget.dart b/lib/models/widget.dart index 52dc23a..b25bde2 100644 --- a/lib/models/widget.dart +++ b/lib/models/widget.dart @@ -20,10 +20,27 @@ class CommonMessage with _$CommonMessage { } @freezed -class CommonAppBarState with _$CommonAppBarState { - const factory CommonAppBarState({ +class AppBarState with _$AppBarState { + const factory AppBarState({ @Default([]) List actions, - Function(String)? onSearch, - @Default(false) bool searching, - }) = _CommonAppBarState; + AppBarSearchState? searchState, + AppBarEditState? editState, + }) = _AppBarState; +} + +@freezed +class AppBarSearchState with _$AppBarSearchState { + const factory AppBarSearchState({ + required Function(String) onSearch, + @Default(false) bool isSearch, + }) = _AppBarSearchState; +} + +@freezed +class AppBarEditState with _$AppBarEditState { + const factory AppBarEditState({ + @Default(0) editCount, + @Default(false) bool isEdit, + required Function() onExit, + }) = _AppBarEditState; } diff --git a/lib/pages/editor.dart b/lib/pages/editor.dart index 0b195e4..88e340b 100644 --- a/lib/pages/editor.dart +++ b/lib/pages/editor.dart @@ -1,16 +1,21 @@ 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/app.dart'; +import 'package:fl_clash/widgets/pop_scope.dart'; +import 'package:fl_clash/widgets/popup.dart'; import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:fl_clash/widgets/scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:re_editor/re_editor.dart'; import 'package:re_highlight/languages/yaml.dart'; import 'package:re_highlight/styles/atom-one-light.dart'; -import '../models/common.dart'; - typedef EditingValueChangeBuilder = Widget Function(CodeLineEditingValue value); -class EditorPage extends StatefulWidget { +class EditorPage extends ConsumerStatefulWidget { final String title; final String content; final Function(BuildContext context, String text)? onSave; @@ -25,17 +30,19 @@ class EditorPage extends StatefulWidget { }); @override - State createState() => _EditorPageState(); + ConsumerState createState() => _EditorPageState(); } -class _EditorPageState extends State { +class _EditorPageState extends ConsumerState { late CodeLineEditingController _controller; + late CodeFindController _findController; final _focusNode = FocusNode(); @override void initState() { super.initState(); _controller = CodeLineEditingController.fromText(widget.content); + _findController = CodeFindController(_controller); if (system.isDesktop) { return; } @@ -65,6 +72,7 @@ class _EditorPageState extends State { @override void dispose() { + _findController.dispose(); _controller.dispose(); _focusNode.dispose(); super.dispose(); @@ -79,35 +87,26 @@ class _EditorPageState extends State { ); } + _handleSearch() { + _findController.findMode(); + } + @override Widget build(BuildContext context) { - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, _) async { - if (didPop) return; - if (widget.onPop != null) { - final res = await widget.onPop!(context, _controller.text); - if (res && context.mounted) { - Navigator.of(context).pop(); - } - return; + final isMobileView = ref.watch(isMobileViewProvider); + return CommonPopScope( + onPop: () async { + if (widget.onPop == null) { + return true; } - Navigator.of(context).pop(); + final res = await widget.onPop!(context, _controller.text); + if (res && context.mounted) { + return true; + } + return false; }, child: CommonScaffold( actions: [ - _wrapController( - (value) => IconButton( - onPressed: _controller.canUndo ? _controller.undo : null, - icon: const Icon(Icons.undo), - ), - ), - _wrapController( - (value) => IconButton( - onPressed: _controller.canRedo ? _controller.redo : null, - icon: const Icon(Icons.redo), - ), - ), if (widget.onSave != null) _wrapController( (value) => IconButton( @@ -119,15 +118,50 @@ class _EditorPageState extends State { icon: const Icon(Icons.save_sharp), ), ), + _wrapController( + (value) => CommonPopupBox( + targetBuilder: (open) { + return IconButton( + onPressed: open, + icon: const Icon(Icons.more_vert), + ); + }, + popup: CommonPopupMenu( + items: [ + PopupMenuItemData( + icon: Icons.search, + label: appLocalizations.search, + onPressed: _handleSearch, + ), + PopupMenuItemData( + icon: Icons.undo, + label: appLocalizations.undo, + onPressed: _controller.canUndo ? _controller.undo : null, + ), + PopupMenuItemData( + icon: Icons.redo, + label: appLocalizations.redo, + onPressed: _controller.canRedo ? _controller.redo : null, + ), + ], + ), + ), + ), ], body: CodeEditor( + findController: _findController, + findBuilder: (context, controller, readOnly) => FindPanel( + controller: controller, + readOnly: readOnly, + isMobileView: isMobileView, + ), + padding: EdgeInsets.only( + right: 16, + ), focusNode: _focusNode, scrollbarBuilder: (context, child, details) { - return Scrollbar( + return CommonScrollBar( controller: details.controller, - thickness: 8, - radius: const Radius.circular(2), - interactive: true, child: child, ); }, @@ -156,6 +190,7 @@ class _EditorPageState extends State { controller: _controller, style: CodeEditorStyle( fontSize: 14, + fontFamily: FontFamily.jetBrainsMono.value, codeTheme: CodeHighlightTheme( languages: { 'yaml': CodeHighlightThemeMode( @@ -172,6 +207,247 @@ class _EditorPageState extends State { } } +const double _kDefaultFindPanelHeight = 52; + +class FindPanel extends StatelessWidget implements PreferredSizeWidget { + final CodeFindController controller; + final bool readOnly; + final bool isMobileView; + final double height; + + const FindPanel({ + super.key, + required this.controller, + required this.readOnly, + required this.isMobileView, + }) : height = (isMobileView + ? _kDefaultFindPanelHeight * 2 + : _kDefaultFindPanelHeight) + + 8; + + @override + Size get preferredSize => Size( + double.infinity, + controller.value == null ? 0 : height, + ); + + @override + Widget build(BuildContext context) { + if (controller.value == null) { + return const SizedBox( + width: 0, + height: 0, + ); + } + return Container( + padding: EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + margin: EdgeInsets.only( + bottom: 8, + ), + color: context.colorScheme.surface, + alignment: Alignment.centerLeft, + height: height, + child: _buildFindInputView(context), + ); + } + + Widget _buildFindInputView(BuildContext context) { + final CodeFindValue value = controller.value!; + final String result; + if (value.result == null) { + result = appLocalizations.none; + } else { + result = '${value.result!.index + 1}/${value.result!.matches.length}'; + } + final bar = Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!isMobileView) ...[ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 360), + child: _buildFindInput(context, value), + ), + SizedBox( + width: 12, + ), + ], + Text( + result, + style: context.textTheme.bodyMedium, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + spacing: 8, + children: [ + _buildIconButton( + onPressed: value.result == null + ? null + : () { + controller.previousMatch(); + }, + icon: Icons.arrow_upward, + ), + _buildIconButton( + onPressed: value.result == null + ? null + : () { + controller.nextMatch(); + }, + icon: Icons.arrow_downward, + ), + SizedBox( + width: 2, + ), + IconButton.filledTonal( + visualDensity: VisualDensity.compact, + onPressed: controller.close, + style: ButtonStyle( + padding: WidgetStatePropertyAll( + EdgeInsets.all(0), + ), + ), + icon: Icon( + Icons.close, + size: 16, + ), + ), + ], + ), + ), + ], + ); + if (isMobileView) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + bar, + SizedBox( + height: 4, + ), + _buildFindInput(context, value), + ], + ); + } + return bar; + } + + _buildFindInput(BuildContext context, CodeFindValue value) { + return Stack( + alignment: Alignment.center, + children: [ + _buildTextField( + context: context, + onSubmitted: () { + if (value.result == null) { + return; + } + controller.nextMatch(); + controller.findInputFocusNode.requestFocus(); + }, + controller: controller.findInputController, + focusNode: controller.findInputFocusNode, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + spacing: 8, + children: [ + _buildCheckText( + context: context, + text: 'Aa', + isSelected: value.option.caseSensitive, + onPressed: () { + controller.toggleCaseSensitive(); + }, + ), + _buildCheckText( + context: context, + text: '.*', + isSelected: value.option.regex, + onPressed: () { + controller.toggleRegex(); + }, + ), + SizedBox( + width: 4, + ), + ], + ) + ], + ); + } + + Widget _buildTextField({ + required BuildContext context, + required TextEditingController controller, + required FocusNode focusNode, + required VoidCallback onSubmitted, + }) { + return TextField( + maxLines: 1, + focusNode: focusNode, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + ), + ), + onSubmitted: (_) { + onSubmitted(); + }, + controller: controller, + ); + } + + Widget _buildCheckText({ + required BuildContext context, + required String text, + required bool isSelected, + required VoidCallback onPressed, + }) { + return SizedBox( + width: 28, + height: 28, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: isSelected + ? IconButton.filledTonal( + onPressed: onPressed, + padding: EdgeInsets.all(2), + icon: Text(text, style: context.textTheme.bodySmall), + ) + : IconButton( + onPressed: onPressed, + padding: EdgeInsets.all(2), + icon: Text( + text, + style: context.textTheme.bodySmall, + ), + ), + ), + ); + } + + Widget _buildIconButton({ + required IconData icon, + VoidCallback? onPressed, + }) { + return IconButton( + visualDensity: VisualDensity.compact, + onPressed: onPressed, + style: ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.all(0))), + icon: Icon( + icon, + size: 16, + ), + ); + } +} + class ContextMenuControllerImpl implements SelectionToolbarController { OverlayEntry? _overlayEntry; bool _isFirstRender = true; @@ -205,23 +481,23 @@ class ContextMenuControllerImpl implements SelectionToolbarController { final isNotEmpty = controller.selectedText.isNotEmpty; final isAllSelected = controller.isAllSelected; final hasSelected = controller.selectedText.isNotEmpty; - List menus = [ + List menus = [ if (isNotEmpty) - ActionItemData( + PopupMenuItemData( label: appLocalizations.copy, onPressed: controller.copy, ), - ActionItemData( + PopupMenuItemData( label: appLocalizations.paste, onPressed: controller.paste, ), if (isNotEmpty) - ActionItemData( + PopupMenuItemData( label: appLocalizations.cut, onPressed: controller.cut, ), if (hasSelected && !isAllSelected) - ActionItemData( + PopupMenuItemData( label: appLocalizations.selectAll, onPressed: controller.selectAll, ), @@ -235,7 +511,7 @@ class ContextMenuControllerImpl implements SelectionToolbarController { anchorAbove: anchors.primaryAnchor, anchorBelow: anchors.secondaryAnchor ?? Offset.zero, children: menus.asMap().entries.map( - (MapEntry entry) { + (MapEntry entry) { return TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.getPadding( entry.key, @@ -243,7 +519,11 @@ class ContextMenuControllerImpl implements SelectionToolbarController { ), alignment: AlignmentDirectional.centerStart, onPressed: () { - entry.value.onPressed(); + if (entry.value.onPressed == null) { + return; + } + entry.value.onPressed!(); + _removeOverLayEntry(); }, child: Text(entry.value.label), ); diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 19a1ded..ad4452f 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; @@ -15,7 +17,7 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return BackScope( + return HomeBackScope( child: Consumer( builder: (_, ref, child) { final state = ref.watch(homeStateProvider); @@ -59,57 +61,74 @@ class _HomePageView extends ConsumerStatefulWidget { } class _HomePageViewState extends ConsumerState<_HomePageView> { - _updatePageController(List navigationItems) { - final pageLabel = globalState.appState.pageLabel; - final index = navigationItems.lastIndexWhere( - (element) => element.label == pageLabel, + late PageController _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController( + initialPage: _pageIndex, + keepPage: true, ); - final pageIndex = index == -1 ? 0 : index; - if (globalState.pageController != null) { - Future.delayed(Duration(milliseconds: 200), () { - globalState.appController.toPage( - pageIndex, - hasAnimate: true, - ); - }); - } else { - globalState.pageController = PageController( - initialPage: pageIndex, - keepPage: true, + ref.listenManual(currentPageLabelProvider, (prev, next) { + if (prev != next) { + _toPage(next); + } + }); + ref.listenManual(currentNavigationsStateProvider, (prev, next) { + if (prev?.value.length != next.value.length) { + _updatePageController(); + } + }); + } + + int get _pageIndex { + final navigationItems = ref.read(currentNavigationsStateProvider).value; + return navigationItems.indexWhere( + (item) => item.label == globalState.appState.pageLabel, + ); + } + + _toPage(PageLabel pageLabel, [bool ignoreAnimateTo = false]) async { + if (!mounted) { + return; + } + final navigationItems = ref.read(currentNavigationsStateProvider).value; + final index = navigationItems.indexWhere((item) => item.label == pageLabel); + if (index == -1) { + return; + } + final isAnimateToPage = ref.read(appSettingProvider).isAnimateToPage; + final isMobile = ref.read(isMobileViewProvider); + if (isAnimateToPage && isMobile && !ignoreAnimateTo) { + await _pageController.animateToPage( + index, + duration: kTabScrollDuration, + curve: Curves.easeOut, ); + } else { + _pageController.jumpToPage(index); } } - // _handlePageChanged(PageLabel next) { - // debouncer.call(DebounceTag.pageChange, () { - // if (_prevPageLabel == next) { - // return; - // } - // if (_prevPageLabel != null) { - // final prevTabPageKey = GlobalObjectKey(_prevPageLabel!); - // if (prevTabPageKey.currentState is PageMixin) { - // (prevTabPageKey.currentState as PageMixin).onPageHidden(); - // } - // } - // final nextTabPageKey = GlobalObjectKey(next); - // if (nextTabPageKey.currentState is PageMixin) { - // (nextTabPageKey.currentState as PageMixin).onPageShow(); - // } - // _prevPageLabel = next; - // }, duration: commonDuration); - // } + _updatePageController() { + final pageLabel = globalState.appState.pageLabel; + _toPage(pageLabel, true); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final navigationItems = ref.watch(currentNavigationsStateProvider).value; - _updatePageController(navigationItems); return PageView.builder( - controller: globalState.pageController, + controller: _pageController, physics: const NeverScrollableScrollPhysics(), itemCount: navigationItems.length, - // onPageChanged: (index) { - // _handlePageChanged(navigationItems[index].label); - // }, itemBuilder: (_, index) { final navigationItem = navigationItems[index]; return KeepScope( @@ -134,26 +153,8 @@ class CommonNavigationBar extends ConsumerWidget { required this.currentIndex, }); - _updateSafeMessageOffset(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final size = context.size; - if (viewMode == ViewMode.mobile) { - globalState.safeMessageOffsetNotifier.value = Offset( - 0, - -(size?.height ?? 0), - ); - } else { - globalState.safeMessageOffsetNotifier.value = Offset( - size?.width ?? 0, - 0, - ); - } - }); - } - @override Widget build(BuildContext context, ref) { - _updateSafeMessageOffset(context); if (viewMode == ViewMode.mobile) { return NavigationBarTheme( data: _NavigationBarDefaultsM3(context), @@ -166,7 +167,9 @@ class CommonNavigationBar extends ConsumerWidget { ), ) .toList(), - onDestinationSelected: globalState.appController.toPage, + onDestinationSelected: (index) { + globalState.appController.toPage(navigationItems[index].label); + }, selectedIndex: currentIndex, ), ); @@ -205,7 +208,10 @@ class CommonNavigationBar extends ConsumerWidget { ), ) .toList(), - onDestinationSelected: globalState.appController.toPage, + onDestinationSelected: (index) { + globalState.appController + .toPage(navigationItems[index].label); + }, extended: false, selectedIndex: currentIndex, labelType: showLabel @@ -264,7 +270,7 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData { return IconThemeData( size: 24.0, color: states.contains(WidgetState.disabled) - ? _colors.onSurfaceVariant.withOpacity(0.38) + ? _colors.onSurfaceVariant.opacity38 : states.contains(WidgetState.selected) ? _colors.onSecondaryContainer : _colors.onSurfaceVariant, @@ -285,10 +291,35 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData { return style.apply( overflow: TextOverflow.ellipsis, color: states.contains(WidgetState.disabled) - ? _colors.onSurfaceVariant.withOpacity(0.38) + ? _colors.onSurfaceVariant.opacity38 : states.contains(WidgetState.selected) ? _colors.onSurface : _colors.onSurfaceVariant); }); } } + +class HomeBackScope extends StatelessWidget { + final Widget child; + + const HomeBackScope({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + if (Platform.isAndroid) { + return CommonPopScope( + onPop: () async { + final canPop = Navigator.canPop(context); + if (canPop) { + Navigator.pop(context); + } else { + await globalState.appController.handleBackOrExit(); + } + return false; + }, + child: child, + ); + } + return child; + } +} diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart index 114a76a..1c65c60 100644 --- a/lib/pages/scan.dart +++ b/lib/pages/scan.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'package:fl_clash/common/color.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/activate_box.dart'; import 'package:flutter/material.dart'; @@ -188,7 +189,7 @@ class ScannerOverlay extends CustomPainter { ); final backgroundPaint = Paint() - ..color = Colors.black.withOpacity(0.5) + ..color = Colors.black.opacity50 ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; diff --git a/lib/providers/app.dart b/lib/providers/app.dart index 25396dd..63e7762 100644 --- a/lib/providers/app.dart +++ b/lib/providers/app.dart @@ -3,6 +3,7 @@ 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'; part 'generated/app.g.dart'; @@ -182,24 +183,44 @@ class RunTime extends _$RunTime with AutoDisposeNotifierMixin { } @riverpod -class ViewWidth extends _$ViewWidth with AutoDisposeNotifierMixin { +class ViewSize extends _$ViewSize with AutoDisposeNotifierMixin { @override - double build() { - return globalState.appState.viewWidth; + Size build() { + return globalState.appState.viewSize; } @override onUpdate(value) { globalState.appState = globalState.appState.copyWith( - viewWidth: value, + viewSize: value, ); } - ViewMode get viewMode => other.getViewMode(state); + ViewMode get viewMode => other.getViewMode(state.width); bool get isMobileView => viewMode == ViewMode.mobile; } +@riverpod +double viewWidth(Ref ref) { + return ref.watch(viewSizeProvider).width; +} + +@riverpod +ViewMode viewMode(Ref ref) { + return other.getViewMode(ref.watch(viewWidthProvider)); +} + +@riverpod +bool isMobileView(Ref ref) { + return ref.watch(viewModeProvider) == ViewMode.mobile; +} + +@riverpod +double viewHeight(Ref ref) { + return ref.watch(viewSizeProvider).height; +} + @riverpod class Init extends _$Init with AutoDisposeNotifierMixin { @override diff --git a/lib/providers/config.dart b/lib/providers/config.dart index df5a244..597cfe3 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -141,6 +141,15 @@ class Profiles extends _$Profiles with AutoDisposeNotifierMixin { state = profilesTemp; } + updateProfile(String profileId, Profile Function(Profile profile) builder) { + final List profilesTemp = List.from(state); + final index = profilesTemp.indexWhere((element) => element.id == profileId); + if (index != -1) { + profilesTemp[index] = builder(profilesTemp[index]); + } + state = profilesTemp; + } + deleteProfileById(String id) { state = state.where((element) => element.id != id).toList(); } diff --git a/lib/providers/generated/app.g.dart b/lib/providers/generated/app.g.dart index 75d4dc2..8831640 100644 --- a/lib/providers/generated/app.g.dart +++ b/lib/providers/generated/app.g.dart @@ -6,6 +6,70 @@ part of '../app.dart'; // RiverpodGenerator // ************************************************************************** +String _$viewWidthHash() => r'a469c3414170a6616ff3264962e7f160b2edceca'; + +/// See also [viewWidth]. +@ProviderFor(viewWidth) +final viewWidthProvider = AutoDisposeProvider.internal( + viewWidth, + name: r'viewWidthProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$viewWidthHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ViewWidthRef = AutoDisposeProviderRef; +String _$viewModeHash() => r'fbda5aee64803b09b1431b00650ac6e16d044743'; + +/// See also [viewMode]. +@ProviderFor(viewMode) +final viewModeProvider = AutoDisposeProvider.internal( + viewMode, + name: r'viewModeProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$viewModeHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ViewModeRef = AutoDisposeProviderRef; +String _$isMobileViewHash() => r'554c9ed269a02af001e623e596622e2bb2d658e7'; + +/// See also [isMobileView]. +@ProviderFor(isMobileView) +final isMobileViewProvider = AutoDisposeProvider.internal( + isMobileView, + name: r'isMobileViewProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$isMobileViewHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef IsMobileViewRef = AutoDisposeProviderRef; +String _$viewHeightHash() => r'410aee5b41388226ab16737f0e85a56f7e9fe801'; + +/// See also [viewHeight]. +@ProviderFor(viewHeight) +final viewHeightProvider = AutoDisposeProvider.internal( + viewHeight, + name: r'viewHeightProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$viewHeightHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ViewHeightRef = AutoDisposeProviderRef; String _$logsHash() => r'56fb8aa9d62a97b026b749d204576a7384084737'; /// See also [Logs]. @@ -139,21 +203,20 @@ final runTimeProvider = AutoDisposeNotifierProvider.internal( ); typedef _$RunTime = AutoDisposeNotifier; -String _$viewWidthHash() => r'2d492c97b26c9634ee945a8d7da1563d98b830bd'; +String _$viewSizeHash() => r'44a8ff7a1fb1a9ad278b999560bef3ce2c9fea2a'; -/// See also [ViewWidth]. -@ProviderFor(ViewWidth) -final viewWidthProvider = - AutoDisposeNotifierProvider.internal( - ViewWidth.new, - name: r'viewWidthProvider', +/// See also [ViewSize]. +@ProviderFor(ViewSize) +final viewSizeProvider = AutoDisposeNotifierProvider.internal( + ViewSize.new, + name: r'viewSizeProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$viewWidthHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$viewSizeHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$ViewWidth = AutoDisposeNotifier; +typedef _$ViewSize = AutoDisposeNotifier; String _$initHash() => r'7d3f11c8aff7a1924c5ec8886b2cd2cbdda57c3f'; /// See also [Init]. diff --git a/lib/providers/generated/config.g.dart b/lib/providers/generated/config.g.dart index d2cb84f..b185500 100644 --- a/lib/providers/generated/config.g.dart +++ b/lib/providers/generated/config.g.dart @@ -83,7 +83,7 @@ final themeSettingProvider = ); typedef _$ThemeSetting = AutoDisposeNotifier; -String _$profilesHash() => r'c2bc502d31321274a44901b675fbbab60b4519c3'; +String _$profilesHash() => r'2023af6ceaf273df480897561565cb3be8054ded'; /// See also [Profiles]. @ProviderFor(Profiles) diff --git a/lib/providers/generated/state.g.dart b/lib/providers/generated/state.g.dart index 59f2273..8bca145 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'0708f81450e740a471c65d1dd5db0ed0dc702b3c'; +String _$clashConfigStateHash() => r'848f6b2f734d99fb11ec05f73d614be415e9658f'; /// See also [clashConfigState]. @ProviderFor(clashConfigState) @@ -143,7 +143,7 @@ final vpnStateProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef VpnStateRef = AutoDisposeProviderRef; -String _$homeStateHash() => r'4c8a996c43a705f6d089a727563c20ff87e13a97'; +String _$homeStateHash() => r'2829f5d6a8548f8a97253a5437bf5c498b17c9ba'; /// See also [homeState]. @ProviderFor(homeState) @@ -469,7 +469,7 @@ final packageListSelectorStateProvider = typedef PackageListSelectorStateRef = AutoDisposeProviderRef; String _$moreToolsSelectorStateHash() => - r'82837b45198a75af9a9ce49b9c3ef97c6f8e9f87'; + r'd27e3eceec2422ad6b6231cf52b892e63c67e365'; /// See also [moreToolsSelectorState]. @ProviderFor(moreToolsSelectorState) @@ -488,7 +488,7 @@ final moreToolsSelectorStateProvider = // ignore: unused_element typedef MoreToolsSelectorStateRef = AutoDisposeProviderRef; -String _$isCurrentPageHash() => r'562702367f009c7b324395ab0a2ad3464784be8c'; +String _$isCurrentPageHash() => r'7c300770aef90da23109d9fcfc3bf26140d8cd08'; /// See also [isCurrentPage]. @ProviderFor(isCurrentPage) @@ -1630,5 +1630,157 @@ class _GetProxyDescProviderElement extends AutoDisposeProviderElement @override Proxy get proxy => (origin as GetProxyDescProvider).proxy; } + +String _$getProfileOverrideDataHash() => + r'a17ec085f1733b63b123ac08aa7737588c048c5f'; + +/// See also [getProfileOverrideData]. +@ProviderFor(getProfileOverrideData) +const getProfileOverrideDataProvider = GetProfileOverrideDataFamily(); + +/// See also [getProfileOverrideData]. +class GetProfileOverrideDataFamily extends Family { + /// See also [getProfileOverrideData]. + const GetProfileOverrideDataFamily(); + + /// See also [getProfileOverrideData]. + GetProfileOverrideDataProvider call( + String profileId, + ) { + return GetProfileOverrideDataProvider( + profileId, + ); + } + + @override + GetProfileOverrideDataProvider getProviderOverride( + covariant GetProfileOverrideDataProvider provider, + ) { + return call( + provider.profileId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'getProfileOverrideDataProvider'; +} + +/// See also [getProfileOverrideData]. +class GetProfileOverrideDataProvider + extends AutoDisposeProvider { + /// See also [getProfileOverrideData]. + GetProfileOverrideDataProvider( + String profileId, + ) : this._internal( + (ref) => getProfileOverrideData( + ref as GetProfileOverrideDataRef, + profileId, + ), + from: getProfileOverrideDataProvider, + name: r'getProfileOverrideDataProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getProfileOverrideDataHash, + dependencies: GetProfileOverrideDataFamily._dependencies, + allTransitiveDependencies: + GetProfileOverrideDataFamily._allTransitiveDependencies, + profileId: profileId, + ); + + GetProfileOverrideDataProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.profileId, + }) : super.internal(); + + final String profileId; + + @override + Override overrideWith( + OverrideData? Function(GetProfileOverrideDataRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: GetProfileOverrideDataProvider._internal( + (ref) => create(ref as GetProfileOverrideDataRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + profileId: profileId, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _GetProfileOverrideDataProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is GetProfileOverrideDataProvider && + other.profileId == profileId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, profileId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin GetProfileOverrideDataRef on AutoDisposeProviderRef { + /// The parameter `profileId` of this provider. + String get profileId; +} + +class _GetProfileOverrideDataProviderElement + extends AutoDisposeProviderElement + with GetProfileOverrideDataRef { + _GetProfileOverrideDataProviderElement(super.provider); + + @override + String get profileId => (origin as GetProfileOverrideDataProvider).profileId; +} + +String _$profileOverrideStateHash() => + r'16d7c75849ed077d60553e5d2bba4ed54b307971'; + +/// See also [ProfileOverrideState]. +@ProviderFor(ProfileOverrideState) +final profileOverrideStateProvider = AutoDisposeNotifierProvider< + ProfileOverrideState, ProfileOverrideStateModel>.internal( + ProfileOverrideState.new, + name: r'profileOverrideStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$profileOverrideStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ProfileOverrideState = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/providers/state.dart b/lib/providers/state.dart index baf96c6..42b5ff1 100644 --- a/lib/providers/state.dart +++ b/lib/providers/state.dart @@ -72,9 +72,13 @@ 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, + )); return ClashConfigState( overrideDns: overrideDns, clashConfig: clashConfig, + overrideData: overrideData ?? OverrideData(), ); } @@ -148,7 +152,7 @@ VpnState vpnState(Ref ref) { HomeState homeState(Ref ref) { final pageLabel = ref.watch(currentPageLabelProvider); final navigationItems = ref.watch(currentNavigationsStateProvider).value; - final viewMode = ref.watch(viewWidthProvider.notifier).viewMode; + final viewMode = ref.watch(viewModeProvider); final locale = ref.watch(appSettingProvider).locale; return HomeState( pageLabel: pageLabel, @@ -295,7 +299,7 @@ PackageListSelectorState packageListSelectorState(Ref ref) { @riverpod MoreToolsSelectorState moreToolsSelectorState(Ref ref) { - final viewMode = ref.watch(viewWidthProvider.notifier).viewMode; + final viewMode = ref.watch(viewModeProvider); final navigationItems = ref.watch(navigationsStateProvider.select((state) { return state.value.where((element) { final isMore = element.modes.contains(NavigationItemMode.more); @@ -322,7 +326,7 @@ bool isCurrentPage( return true; } if (handler != null) { - final viewMode = ref.watch(viewWidthProvider.notifier).viewMode; + final viewMode = ref.watch(viewModeProvider); return handler(currentPageLabel, viewMode); } return false; @@ -417,6 +421,9 @@ ProxyCardState _getProxyCardState( final group = groups[index]; final currentSelectedName = group .getCurrentSelectedName(selectedMap[proxyDelayState.proxyName] ?? ''); + if (currentSelectedName.isEmpty) { + return proxyDelayState; + } return _getProxyCardState( groups, selectedMap, @@ -466,3 +473,34 @@ String getProxyDesc(Ref ref, Proxy proxy) { return "${proxy.type}(${state.proxyName.isNotEmpty ? state.proxyName : '*'})"; } } + +@riverpod +class ProfileOverrideState extends _$ProfileOverrideState { + @override + ProfileOverrideStateModel build() { + return ProfileOverrideStateModel( + isEdit: false, + selectedRules: {}, + ); + } + + updateState( + ProfileOverrideStateModel? Function(ProfileOverrideStateModel state) + builder, + ) { + final value = builder(state); + if (value == null) { + return; + } + state = value; + } +} + +@riverpod +OverrideData? getProfileOverrideData(Ref ref, String profileId) { + return ref.watch( + profilesProvider.select( + (state) => state.getProfile(profileId)?.overrideData, + ), + ); +} diff --git a/lib/state.dart b/lib/state.dart index 9d3e692..985d309 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'package:animations/animations.dart'; import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/theme.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/l10n/l10n.dart'; 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:package_info_plus/package_info_plus.dart'; @@ -23,13 +25,13 @@ class GlobalState { Timer? groupsUpdateTimer; late Config config; late AppState appState; + bool isPre = true; late PackageInfo packageInfo; Function? updateCurrentDelayDebounce; - PageController? pageController; late Measure measure; + late CommonTheme theme; DateTime? startTime; UpdateTasks tasks = []; - final safeMessageOffsetNotifier = ValueNotifier(Offset.zero); final navigatorKey = GlobalKey(); late AppController appController; GlobalKey homeScaffoldKey = GlobalKey(); @@ -46,7 +48,7 @@ class GlobalState { initApp(int version) async { appState = AppState( version: version, - viewWidth: other.getScreenSize().width, + viewSize: Size.zero, requests: FixedList(1000), logs: FixedList(1000), traffics: FixedList(30), @@ -114,7 +116,7 @@ class GlobalState { } Future showMessage({ - required String title, + String? title, required InlineSpan message, String? confirmText, bool cancelable = true, @@ -122,23 +124,8 @@ class GlobalState { return await showCommonDialog( child: Builder( builder: (context) { - return AlertDialog( - title: Text(title), - content: Container( - width: 300, - constraints: const BoxConstraints(maxHeight: 200), - child: SingleChildScrollView( - child: SelectableText.rich( - TextSpan( - style: Theme.of(context).textTheme.labelLarge, - children: [message], - ), - style: const TextStyle( - overflow: TextOverflow.visible, - ), - ), - ), - ), + return CommonDialog( + title: title ?? appLocalizations.tip, actions: [ if (cancelable) TextButton( @@ -154,6 +141,21 @@ class GlobalState { child: Text(confirmText ?? appLocalizations.confirm), ) ], + child: Container( + width: 300, + constraints: const BoxConstraints(maxHeight: 200), + child: SingleChildScrollView( + child: SelectableText.rich( + TextSpan( + style: Theme.of(context).textTheme.labelLarge, + children: [message], + ), + style: const TextStyle( + overflow: TextOverflow.visible, + ), + ), + ), + ), ); }, ), @@ -171,7 +173,7 @@ class GlobalState { barrierDismissible: dismissible, ), builder: (_) => child, - filter: filter, + filter: commonFilter, ); } @@ -251,12 +253,17 @@ class GlobalState { ? defaultBypassPrivateRouteAddress : clashConfig.tun.routeAddress, ), + rule: currentProfile?.overrideData.runningRule ?? [], ), params: ConfigExtendedParams( isPatch: isPatch ?? false, selectedMap: currentProfile?.selectedMap ?? {}, overrideDns: config.overrideDns, testUrl: config.appSetting.testUrl, + overrideRule: + currentProfile?.overrideData.rule.type == OverrideRuleType.override + ? true + : false, ), ); } diff --git a/lib/widgets/back_scope.dart b/lib/widgets/back_scope.dart deleted file mode 100644 index 5f36330..0000000 --- a/lib/widgets/back_scope.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:io'; -import 'package:fl_clash/state.dart'; -import 'package:flutter/widgets.dart'; - -class BackScope extends StatefulWidget { - final Widget child; - - const BackScope({super.key, required this.child}); - - @override - State createState() => _PopContainerState(); -} - -class _PopContainerState extends State { - @override - Widget build(BuildContext context) { - if (Platform.isAndroid) { - return PopScope( - canPop: false, - onPopInvokedWithResult: (_, __) async { - final canPop = Navigator.canPop(context); - if (canPop) { - Navigator.pop(context); - } else { - await globalState.appController.handleBackOrExit(); - } - }, - child: widget.child, - ); - } - return widget.child; - } -} diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 9879140..d68c93b 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -85,14 +85,13 @@ class CommonCard extends StatelessWidget { const CommonCard({ super.key, bool? isSelected, - this.type = CommonCardType.filled, + this.type = CommonCardType.plain, this.onPressed, this.selectWidget, - this.backgroundColor, this.radius = 12, required this.child, + this.padding, this.enterAnimated = false, - this.borderSide, this.info, }) : isSelected = isSelected ?? false; @@ -101,20 +100,22 @@ class CommonCard extends StatelessWidget { final void Function()? onPressed; final Widget? selectWidget; final Widget child; + final EdgeInsets? padding; final Info? info; final CommonCardType type; final double radius; - final WidgetStateProperty? backgroundColor; - final WidgetStateProperty? borderSide; + + // final WidgetStateProperty? backgroundColor; + // final WidgetStateProperty? borderSide; BorderSide getBorderSide(BuildContext context, Set states) { final colorScheme = context.colorScheme; - // if (type == CommonCardType.filled) { - // return BorderSide.none; - // } + if (type == CommonCardType.filled) { + return BorderSide.none; + } final hoverColor = isSelected - ? colorScheme.primary.toLight - : colorScheme.primary.toLighter; + ? colorScheme.primary.opacity80 + : colorScheme.primary.opacity60; if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused) || states.contains(WidgetState.pressed)) { @@ -123,31 +124,21 @@ class CommonCard extends StatelessWidget { ); } return BorderSide( - color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft, + color: isSelected + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, ); } Color? getBackgroundColor(BuildContext context, Set states) { - final colorScheme = context.colorScheme; - switch (type) { - case CommonCardType.plain: - if (isSelected) { - return colorScheme.secondaryContainer; - } - if (states.isEmpty) { - return colorScheme.surface; - } - return Theme.of(context) - .outlinedButtonTheme - .style - ?.backgroundColor - ?.resolve(states); - case CommonCardType.filled: - if (isSelected) { - return colorScheme.secondaryContainer; - } - return colorScheme.surfaceContainer; + if (type == CommonCardType.filled) { + return context.colorScheme.surfaceContainer; } + final colorScheme = context.colorScheme; + if (isSelected) { + return colorScheme.secondaryContainer; + } + return colorScheme.surfaceContainerLow; } @override @@ -186,6 +177,7 @@ class CommonCard extends StatelessWidget { } final card = OutlinedButton( + onLongPress: null, clipBehavior: Clip.antiAlias, style: ButtonStyle( padding: const WidgetStatePropertyAll(EdgeInsets.zero), @@ -196,14 +188,12 @@ class CommonCard extends StatelessWidget { ), iconColor: WidgetStatePropertyAll(context.colorScheme.primary), iconSize: WidgetStateProperty.all(20), - backgroundColor: backgroundColor ?? - WidgetStateProperty.resolveWith( - (states) => getBackgroundColor(context, states), - ), - side: borderSide ?? - WidgetStateProperty.resolveWith( - (states) => getBorderSide(context, states), - ), + backgroundColor: WidgetStateProperty.resolveWith( + (states) => getBackgroundColor(context, states), + ), + side: WidgetStateProperty.resolveWith( + (states) => getBorderSide(context, states), + ), ), onPressed: onPressed, child: childWidget, @@ -236,3 +226,36 @@ class SelectIcon extends StatelessWidget { ); } } + +class SettingsBlock extends StatelessWidget { + final String title; + final List settings; + + const SettingsBlock({ + super.key, + required this.title, + required this.settings, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(8), + child: Column( + children: [ + InfoHeader( + info: Info( + label: title, + ), + ), + Card( + color: context.colorScheme.surfaceContainer, + child: Column( + children: settings, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/chip.dart b/lib/widgets/chip.dart index eac7ab7..5e81db5 100644 --- a/lib/widgets/chip.dart +++ b/lib/widgets/chip.dart @@ -1,3 +1,4 @@ +import 'package:fl_clash/common/color.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; @@ -28,7 +29,7 @@ class CommonChip extends StatelessWidget { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onDeleted: onPressed ?? () {}, side: - BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), + BorderSide(color: Theme.of(context).dividerColor.opacity15), labelStyle: Theme.of(context).textTheme.bodyMedium, label: Text(label), ); @@ -42,7 +43,7 @@ class CommonChip extends StatelessWidget { horizontal: 4, ), onPressed: onPressed ?? () {}, - side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.2)), + side: BorderSide(color: Theme.of(context).dividerColor.opacity15), labelStyle: Theme.of(context).textTheme.bodyMedium, label: Text(label), ); diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart new file mode 100644 index 0000000..2c96937 --- /dev/null +++ b/lib/widgets/dialog.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +import 'package:fl_clash/providers/app.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class CommonDialog extends ConsumerWidget { + final String title; + final Widget? child; + final List? actions; + final EdgeInsets? padding; + final bool overrideScroll; + final Color? backgroundColor; + + const CommonDialog({ + super.key, + required this.title, + this.actions, + this.child, + this.padding, + this.overrideScroll = false, + this.backgroundColor, + }); + + @override + Widget build(BuildContext context, ref) { + final size = ref.watch(viewSizeProvider); + return AlertDialog( + title: Text(title), + actions: actions, + contentPadding: padding, + backgroundColor: backgroundColor, + content: Container( + constraints: BoxConstraints( + maxHeight: min( + size.height - 40, + 500, + ), + maxWidth: 300, + ), + width: size.width - 40, + child: !overrideScroll + ? SingleChildScrollView( + child: child, + ) + : child, + ), + ); + } +} diff --git a/lib/widgets/donut_chart.dart b/lib/widgets/donut_chart.dart index aa6793d..3e41685 100644 --- a/lib/widgets/donut_chart.dart +++ b/lib/widgets/donut_chart.dart @@ -115,11 +115,9 @@ class DonutChartPainter extends CustomPainter { List get interpolatedData { if (oldData.length != newData.length) return newData; - final interpolatedData = List.generate(newData.length, (index) { final oldValue = oldData[index].value; final newValue = newData[index].value; - final logOldValue = _logTransform(oldValue); final logNewValue = _logTransform(newValue); final interpolatedLogValue = diff --git a/lib/widgets/effect.dart b/lib/widgets/effect.dart new file mode 100644 index 0000000..5044ba0 --- /dev/null +++ b/lib/widgets/effect.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class EffectGestureDetector extends StatefulWidget { + final Widget child; + final GestureLongPressCallback? onLongPress; + final GestureTapCallback? onTap; + + const EffectGestureDetector({ + super.key, + required this.child, + this.onLongPress, + this.onTap, + }); + + @override + State createState() => _EffectGestureDetectorState(); +} + +class _EffectGestureDetectorState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + double _scale = 1; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedScale( + scale: _scale, + duration: kThemeAnimationDuration, + curve: Curves.easeOut, + child: GestureDetector( + onLongPress: widget.onLongPress, + onLongPressStart: (_) { + setState(() { + _scale = 0.95; + }); + }, + onTap: widget.onTap, + onLongPressEnd: (_) { + setState(() { + _scale = 1; + }); + }, + child: widget.child, + ), + ); + } +} diff --git a/lib/widgets/fade_box.dart b/lib/widgets/fade_box.dart index 06ff0dc..956e17c 100644 --- a/lib/widgets/fade_box.dart +++ b/lib/widgets/fade_box.dart @@ -4,10 +4,12 @@ import 'package:flutter/material.dart'; class FadeBox extends StatelessWidget { final Widget child; + final Alignment? alignment; const FadeBox({ super.key, required this.child, + this.alignment, }); @override @@ -19,7 +21,38 @@ class FadeBox extends StatelessWidget { secondaryAnimation, ) { return Container( - alignment: Alignment.centerLeft, + alignment: alignment ?? Alignment.centerLeft, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: child, + ); + } +} + +class FadeThroughBox extends StatelessWidget { + final Widget child; + final Alignment? alignment; + + const FadeThroughBox({ + super.key, + required this.child, + this.alignment, + }); + + @override + Widget build(BuildContext context) { + return PageTransitionSwitcher( + transitionBuilder: ( + child, + animation, + secondaryAnimation, + ) { + return Container( + alignment: alignment ?? Alignment.centerLeft, child: FadeThroughTransition( animation: animation, fillColor: Colors.transparent, diff --git a/lib/widgets/input.dart b/lib/widgets/input.dart index c55a1e1..c67897c 100644 --- a/lib/widgets/input.dart +++ b/lib/widgets/input.dart @@ -1,7 +1,8 @@ import 'package:fl_clash/common/app_localizations.dart'; -import 'package:fl_clash/common/constant.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/dialog.dart'; import 'package:fl_clash/widgets/null_status.dart'; import 'package:flutter/material.dart'; @@ -9,7 +10,7 @@ import 'card.dart'; import 'float_layout.dart'; import 'list.dart'; -class OptionsDialog extends StatelessWidget { +class OptionsDialog extends StatefulWidget { final String title; final List options; final T value; @@ -23,38 +24,77 @@ class OptionsDialog extends StatelessWidget { required this.value, }); + @override + State> createState() => _OptionsDialogState(); +} + +class _OptionsDialogState extends State> { + final _defaultValue = ""; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final context = + GlobalObjectKey(widget.value ?? _defaultValue).currentContext; + if (context != null) { + Scrollable.ensureVisible(context); + } + }); + } + @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(title), - contentPadding: const EdgeInsets.symmetric( + return CommonDialog( + title: widget.title, + padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 16, ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - for (final option in options) - ListItem.radio( - delegate: RadioDelegate( - value: option, - groupValue: value, - onChanged: (T? value) { - Navigator.of(context).pop(value); - }, - ), - title: Text( - this.textBuilder(option), - ), + child: Wrap( + children: [ + for (final option in widget.options) + ListItem.radio( + key: GlobalObjectKey(option ?? _defaultValue), + delegate: RadioDelegate( + value: option, + groupValue: widget.value, + onChanged: (T? value) { + Navigator.of(context).pop(value); + }, ), - ], - ), + title: Text( + widget.textBuilder(option), + ), + ), + ], ), ); } } +class CommonCheckBox extends StatelessWidget { + final bool? value; + final ValueChanged? onChanged; + final bool isCircle; + + const CommonCheckBox({ + required this.value, + required this.onChanged, + this.isCircle = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Checkbox( + shape: isCircle ? const CircleBorder() : null, + value: value, + onChanged: onChanged, + ); + } +} + class InputDialog extends StatefulWidget { final String title; final String value; @@ -104,28 +144,8 @@ class _InputDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(title), - content: SizedBox( - width: 300, - child: Wrap( - runSpacing: 16, - children: [ - TextField( - maxLines: 1, - minLines: 1, - controller: textController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - suffixText: suffixText, - ), - onSubmitted: (_) { - _handleUpdate(); - }, - ), - ], - ), - ), + return CommonDialog( + title: title, actions: [ if (widget.resetValue != null && textController.value.text != widget.resetValue) ...[ @@ -142,90 +162,93 @@ class _InputDialogState extends State { child: Text(appLocalizations.submit), ) ], + child: Wrap( + runSpacing: 16, + children: [ + TextField( + maxLines: 1, + minLines: 1, + controller: textController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixText: suffixText, + ), + onSubmitted: (_) { + _handleUpdate(); + }, + ), + ], + ), ); } } -class ListPage extends StatelessWidget { +class ListInputPage extends StatelessWidget { final String title; - final Iterable items; - final Key Function(T item)? keyBuilder; - final Widget Function(T item) titleBuilder; - final Widget Function(T item)? subtitleBuilder; - final Widget Function(T item)? leadingBuilder; - final String? keyLabel; + final List items; + final Widget Function(String item) titleBuilder; + final Widget Function(String item)? subtitleBuilder; + final Widget Function(String item)? leadingBuilder; final String? valueLabel; - final Function(Iterable items) onChange; + final Function(List items) onChange; - const ListPage({ + const ListInputPage({ super.key, required this.title, required this.items, - this.keyBuilder, required this.titleBuilder, required this.onChange, this.leadingBuilder, - this.keyLabel, this.valueLabel, this.subtitleBuilder, }); - bool get isMap => items is Iterable; + _handleAddOrEdit([String? item]) async { + uniqueValidator(String? value) { + final index = items.indexWhere( + (entry) { + return entry == value; + }, + ); + final current = item == value; + if (index != -1 && !current) { + return appLocalizations.valueExists; + } + return null; + } - _handleAddOrEdit([T? item]) async { - final value = await globalState.showCommonDialog( + final valueField = Field( + label: valueLabel ?? appLocalizations.value, + value: item ?? "", + validator: uniqueValidator, + ); + final value = await globalState.showCommonDialog( child: AddDialog( - keyField: isMap - ? Field( - label: this.keyLabel ?? appLocalizations.key, - value: - item == null ? "" : (item as MapEntry).key, - ) - : null, - valueField: Field( - label: this.valueLabel ?? appLocalizations.value, - value: item == null - ? "" - : isMap - ? (item as MapEntry).value - : item as String, - ), + valueField: valueField, title: title, ), ); if (value == null) return; - final entries = List.from( - items, + final index = items.indexWhere( + (entry) { + return entry == item; + }, ); + final nextItems = List.from(items); if (item != null) { - final index = entries.indexWhere( - (entry) { - if (isMap) { - return (entry as MapEntry).key == - (item as MapEntry).key; - } - return entry == item; - }, - ); - if (index != -1) { - entries[index] = value; - } + nextItems[index] = value; } else { - entries.add(value); + nextItems.add(value); } - onChange(entries); + onChange(nextItems); } - _handleDelete(T item) { - final entries = List.from( + _handleDelete(String? item) { + final entries = List.from( items, ); final index = entries.indexWhere( (entry) { - if (isMap) { - return (entry as MapEntry).key == - (item as MapEntry).key; - } return entry == item; }, ); @@ -235,89 +258,6 @@ class ListPage extends StatelessWidget { onChange(entries); } - Widget _buildList() { - final items = this.items.toList(); - if (this.keyBuilder != null) { - return ReorderableListView.builder( - padding: const EdgeInsets.only( - bottom: 16 + 64, - left: 16, - right: 16, - ), - buildDefaultDragHandles: false, - itemCount: items.length, - itemBuilder: (_, index) { - final e = items[index]; - return Padding( - key: keyBuilder!(e), - padding: const EdgeInsets.symmetric(vertical: 8), - child: ReorderableDragStartListener( - index: index, - child: CommonCard( - child: ListItem( - leading: leadingBuilder != null ? leadingBuilder!(e) : null, - title: titleBuilder(e), - subtitle: - subtitleBuilder != null ? subtitleBuilder!(e) : null, - trailing: IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () { - _handleDelete(e); - }, - ), - ), - onPressed: () { - _handleAddOrEdit(e); - }, - ), - ), - ); - }, - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final nextItems = List.from(items); - final item = nextItems.removeAt(oldIndex); - nextItems.insert(newIndex, item); - onChange(nextItems); - }, - ); - } else { - return ListView.builder( - padding: const EdgeInsets.only( - bottom: 16 + 64, - left: 16, - right: 16, - ), - itemCount: items.length, - itemBuilder: (_, index) { - final e = items[index]; - return Padding( - key: ObjectKey(e.toString()), - padding: const EdgeInsets.symmetric(vertical: 8), - child: CommonCard( - child: ListItem( - leading: leadingBuilder != null ? leadingBuilder!(e) : null, - title: titleBuilder(e), - subtitle: subtitleBuilder != null ? subtitleBuilder!(e) : null, - trailing: IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () { - _handleDelete(e); - }, - ), - ), - onPressed: () { - _handleAddOrEdit(e); - }, - ), - ); - }, - ); - } - } - @override Widget build(BuildContext context) { return FloatLayout( @@ -331,7 +271,204 @@ class ListPage extends StatelessWidget { ), child: items.isEmpty ? NullStatus(label: appLocalizations.noData) - : _buildList(), + : ReorderableListView.builder( + padding: const EdgeInsets.only( + bottom: 16 + 64, + left: 16, + right: 16, + ), + buildDefaultDragHandles: false, + itemCount: items.length, + itemBuilder: (context, index) { + final e = items[index]; + return Padding( + key: ValueKey(e), + padding: const EdgeInsets.symmetric(vertical: 6), + child: ReorderableDragStartListener( + index: index, + child: CommonCard( + child: ListItem( + leading: + leadingBuilder != null ? leadingBuilder!(e) : null, + title: titleBuilder(e), + subtitle: subtitleBuilder != null + ? subtitleBuilder!(e) + : null, + trailing: IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () { + _handleDelete(e); + }, + ), + ), + onPressed: () { + _handleAddOrEdit(e); + }, + ), + ), + ); + }, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final nextItems = List.from(items); + final item = nextItems.removeAt(oldIndex); + nextItems.insert(newIndex, item); + onChange(nextItems); + }, + ), + ); + } +} + +class MapInputPage extends StatelessWidget { + final String title; + final Map map; + final Widget Function(MapEntry item) titleBuilder; + final Widget Function(MapEntry item)? subtitleBuilder; + final Widget Function(MapEntry item)? leadingBuilder; + final String? keyLabel; + final String? valueLabel; + final Function(Map items) onChange; + + const MapInputPage({ + super.key, + required this.title, + required this.map, + required this.titleBuilder, + required this.onChange, + this.leadingBuilder, + this.keyLabel, + this.valueLabel, + this.subtitleBuilder, + }); + + List> get items => + List>.from( + map.entries, + ); + + _handleAddOrEdit([MapEntry? item]) async { + uniqueValidator(String? value) { + final index = items.indexWhere( + (entry) { + return entry.key == value; + }, + ); + final current = item?.key == value; + if (index != -1 && !current) { + return appLocalizations.keyExists; + } + return null; + } + + final keyField = Field( + label: keyLabel ?? appLocalizations.key, + value: item == null ? "" : item.key, + validator: uniqueValidator, + ); + + final valueField = Field( + label: valueLabel ?? appLocalizations.value, + value: item == null ? "" : item.value, + ); + + final value = await globalState.showCommonDialog>( + child: AddDialog( + keyField: keyField, + valueField: valueField, + title: title, + ), + ); + if (value == null) return; + final index = items.indexWhere( + (entry) { + return entry.key == item?.key; + }, + ); + + final nextItems = List>.from(items); + if (item != null) { + nextItems[index] = value; + } else { + nextItems.add(value); + } + onChange(Map.fromEntries(nextItems)); + } + + _handleDelete(MapEntry item) { + final index = items.indexWhere( + (entry) { + return entry.key == item.key; + }, + ); + if (index != -1) { + items.removeAt(index); + } + onChange(Map.fromEntries(items)); + } + + @override + Widget build(BuildContext context) { + return FloatLayout( + floatingWidget: FloatWrapper( + child: FloatingActionButton( + onPressed: () async { + _handleAddOrEdit(); + }, + child: const Icon(Icons.add), + ), + ), + child: items.isEmpty + ? NullStatus(label: appLocalizations.noData) + : ReorderableListView.builder( + padding: const EdgeInsets.only( + bottom: 16 + 64, + left: 16, + right: 16, + ), + buildDefaultDragHandles: false, + itemCount: items.length, + itemBuilder: (_, index) { + final e = items[index]; + return Padding( + key: ValueKey(e.key), + padding: const EdgeInsets.symmetric(vertical: 6), + child: ReorderableDragStartListener( + index: index, + child: CommonCard( + child: ListItem( + leading: + leadingBuilder != null ? leadingBuilder!(e) : null, + title: titleBuilder(e), + subtitle: subtitleBuilder != null + ? subtitleBuilder!(e) + : null, + trailing: IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () { + _handleDelete(e); + }, + ), + ), + onPressed: () { + _handleAddOrEdit(e); + }, + ), + ), + ); + }, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final nextItems = List>.from(items); + final item = nextItems.removeAt(oldIndex); + nextItems.insert(newIndex, item); + onChange(Map.fromEntries(nextItems)); + }, + ), ); } } @@ -392,45 +529,31 @@ class _AddDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - title: Text(widget.title), - content: Form( + return CommonDialog( + title: widget.title, + actions: [ + TextButton( + onPressed: _submit, + child: Text(appLocalizations.confirm), + ) + ], + child: Form( key: _formKey, - child: SizedBox( - width: dialogCommonWidth, - child: Wrap( - runSpacing: 16, - children: [ - if (keyField != null) - TextFormField( - maxLines: 2, - minLines: 1, - controller: keyController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: keyField!.label, - ), - validator: (String? value) { - if (keyField!.validator != null) { - return keyField!.validator!(value); - } - if (value == null || value.isEmpty) { - return appLocalizations.notEmpty; - } - return null; - }, - ), + child: Wrap( + runSpacing: 16, + children: [ + if (keyField != null) TextFormField( - maxLines: 3, + maxLines: 2, minLines: 1, - controller: valueController, + controller: keyController, decoration: InputDecoration( border: const OutlineInputBorder(), - labelText: valueField.label, + labelText: keyField!.label, ), validator: (String? value) { - if (valueField.validator != null) { - return valueField.validator!(value); + if (keyField!.validator != null) { + return keyField!.validator!(value); } if (value == null || value.isEmpty) { return appLocalizations.notEmpty; @@ -438,16 +561,27 @@ class _AddDialogState extends State { return null; }, ), - ], - ), + TextFormField( + maxLines: 3, + minLines: 1, + controller: valueController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: valueField.label, + ), + validator: (String? value) { + if (valueField.validator != null) { + return valueField.validator!(value); + } + if (value == null || value.isEmpty) { + return appLocalizations.notEmpty; + } + return null; + }, + ), + ], ), ), - actions: [ - TextButton( - onPressed: _submit, - child: Text(appLocalizations.confirm), - ) - ], ); } } diff --git a/lib/widgets/line_chart.dart b/lib/widgets/line_chart.dart index 5fd3c3f..375bac2 100644 --- a/lib/widgets/line_chart.dart +++ b/lib/widgets/line_chart.dart @@ -1,4 +1,5 @@ import 'dart:ui'; +import 'package:fl_clash/common/color.dart'; import 'package:flutter/material.dart'; class Point { @@ -191,8 +192,8 @@ class LineChartPainter extends CustomPainter { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - color.withOpacity(0.3), - color.withOpacity(0.1), + color.opacity38, + color.opacity10, ], ); diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index a78940e..353bb0b 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -49,28 +49,16 @@ class CheckboxDelegate extends Delegate { class OpenDelegate extends Delegate { final Widget widget; final String title; - final double? extendPageWidth; - final bool isBlur; - final bool isScaffold; + final double? maxWidth; + final Widget? action; + final bool blur; const OpenDelegate({ required this.title, required this.widget, - this.extendPageWidth, - this.isBlur = true, - this.isScaffold = false, - }); -} - -class NextDelegate extends Delegate { - final Widget widget; - final String title; - final double? extendPageWidth; - - const NextDelegate({ - required this.title, - required this.widget, - this.extendPageWidth, + this.maxWidth, + this.action, + this.blur = true, }); } @@ -112,10 +100,12 @@ class ListItem extends StatelessWidget { final Widget? subtitle; final EdgeInsets padding; final ListTileTitleAlignment tileTitleAlignment; - final bool? prue; + final bool? dense; final Widget? trailing; final Delegate delegate; final double? horizontalTitleGap; + final TextStyle? titleTextStyle; + final TextStyle? subtitleTextStyle; final void Function()? onTap; const ListItem({ @@ -126,8 +116,10 @@ class ListItem extends StatelessWidget { this.padding = const EdgeInsets.symmetric(horizontal: 16), this.trailing, this.horizontalTitleGap, - this.prue, + this.dense, this.onTap, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : delegate = const Delegate(); @@ -140,7 +132,9 @@ class ListItem extends StatelessWidget { this.trailing, required OpenDelegate this.delegate, this.horizontalTitleGap, - this.prue, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTap = null; @@ -153,7 +147,9 @@ class ListItem extends StatelessWidget { this.trailing, required OptionsDelegate this.delegate, this.horizontalTitleGap, - this.prue, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTap = null; @@ -166,20 +162,9 @@ class ListItem extends StatelessWidget { this.trailing, required InputDelegate this.delegate, this.horizontalTitleGap, - this.prue, - 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.prue, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTap = null; @@ -191,7 +176,9 @@ class ListItem extends StatelessWidget { this.padding = const EdgeInsets.only(left: 16, right: 8), required CheckboxDelegate this.delegate, this.horizontalTitleGap, - this.prue, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : trailing = null, onTap = null; @@ -204,7 +191,9 @@ class ListItem extends StatelessWidget { this.padding = const EdgeInsets.only(left: 16, right: 8), required SwitchDelegate this.delegate, this.horizontalTitleGap, - this.prue, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : trailing = null, onTap = null; @@ -217,7 +206,9 @@ class ListItem extends StatelessWidget { this.padding = const EdgeInsets.only(left: 12, right: 16), required RadioDelegate this.delegate, this.horizontalTitleGap = 8, - this.prue, + this.dense, + this.titleTextStyle, + this.subtitleTextStyle, this.tileTitleAlignment = ListTileTitleAlignment.center, }) : leading = null, onTap = null; @@ -227,42 +218,11 @@ class ListItem extends StatelessWidget { Widget? trailing, Widget? leading, }) { - if (prue == true) { - final List children = []; - if (leading != null) { - children.add(leading); - children.add( - SizedBox( - width: horizontalTitleGap, - ), - ); - } - children.add( - Expanded( - child: title, - ), - ); - if (trailing != null) { - children.add( - SizedBox( - width: horizontalTitleGap, - ), - ); - children.add(trailing); - } - return InkWell( - splashFactory: NoSplash.splashFactory, - onTap: onTap, - child: Container( - constraints: BoxConstraints.expand(), - padding: padding, - child: Row( - children: children, - ), - ), - ); - } return ListTile( + key: key, + dense: dense, + titleTextStyle: titleTextStyle, + subtitleTextStyle: subtitleTextStyle, leading: leading ?? this.leading, horizontalTitleGap: horizontalTitleGap, title: title, @@ -286,13 +246,22 @@ class ListItem extends StatelessWidget { openAction() { final isMobile = globalState.appState.viewMode == ViewMode.mobile; if (!isMobile) { - showExtendPage( + showExtend( context, - body: child, - title: openDelegate.title, - extendPageWidth: openDelegate.extendPageWidth, - isBlur: openDelegate.isBlur, - isScaffold: openDelegate.isScaffold, + props: ExtendProps( + blur: openDelegate.blur, + maxWidth: openDelegate.maxWidth, + ), + builder: (_, type) { + return AdaptiveSheetScaffold( + actions: [ + if (openDelegate.action != null) openDelegate.action!, + ], + type: type, + body: child, + title: openDelegate.title, + ); + }, ); return; } @@ -345,31 +314,6 @@ class ListItem extends StatelessWidget { }, ); } - if (delegate is NextDelegate) { - final nextDelegate = delegate as NextDelegate; - return _buildListTile( - onTap: () { - final isMobile = globalState.appState.viewMode == ViewMode.mobile; - if (!isMobile) { - showExtendPage( - context, - body: nextDelegate.widget, - title: nextDelegate.title, - extendPageWidth: nextDelegate.extendPageWidth, - ); - return; - } - - BaseNavigator.push( - context, - CommonScaffold( - key: Key(nextDelegate.title), - body: nextDelegate.widget, - title: nextDelegate.title, - )); - }, - ); - } if (delegate is CheckboxDelegate) { final checkboxDelegate = delegate as CheckboxDelegate; return _buildListTile( @@ -378,7 +322,7 @@ class ListItem extends StatelessWidget { checkboxDelegate.onChanged!(!checkboxDelegate.value); } }, - trailing: Checkbox( + trailing: CommonCheckBox( value: checkboxDelegate.value, onChanged: checkboxDelegate.onChanged, ), @@ -427,42 +371,69 @@ class ListItem extends StatelessWidget { class ListHeader extends StatelessWidget { final String title; + final String? subTitle; final List actions; + final EdgeInsets? padding; + final double? space; const ListHeader({ super.key, required this.title, + this.subTitle, + this.padding, List? actions, + this.space, }) : actions = actions ?? const []; @override Widget build(BuildContext context) { return Container( alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), + padding: padding ?? + const EdgeInsets.only( + left: 16, + right: 8, + top: 24, + bottom: 8, + ), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), Expanded( - flex: 1, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...actions, + Text( + title, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .opacity80, + fontWeight: FontWeight.w600, + ), + ), + if (subTitle != null) + Text( + subTitle!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.outline, + ), + ), ], ), ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ...genActions( + actions, + space: space, + ), + ], + ), ], ), ); @@ -524,3 +495,132 @@ Widget generateListView(List items) { ), ); } + +class CacheItemExtentListView extends StatefulWidget { + final NullableIndexedWidgetBuilder itemBuilder; + final int itemCount; + final String Function(int index) keyBuilder; + final double Function(int index) itemExtentBuilder; + final ScrollPhysics? physics; + final bool shrinkWrap; + final bool reverse; + final ScrollController controller; + + const CacheItemExtentListView({ + super.key, + this.physics, + this.reverse = false, + this.shrinkWrap = false, + required this.itemBuilder, + required this.controller, + required this.keyBuilder, + required this.itemCount, + required this.itemExtentBuilder, + }); + + @override + State createState() => + CacheItemExtentListViewState(); +} + +class CacheItemExtentListViewState extends State { + late final FixedMap _cacheHeightMap; + + @override + void initState() { + super.initState(); + _cacheHeightMap = FixedMap(widget.itemCount); + } + + clearCache() { + _cacheHeightMap.clear(); + } + + @override + Widget build(BuildContext context) { + _cacheHeightMap.updateMaxSize(widget.itemCount); + return ListView.builder( + itemBuilder: widget.itemBuilder, + itemCount: widget.itemCount, + physics: widget.physics, + reverse: widget.reverse, + 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)); + }, + ); + } + + @override + void dispose() { + _cacheHeightMap.clear(); + super.dispose(); + } +} + +class CacheItemExtentSliverReorderableList extends StatefulWidget { + final IndexedWidgetBuilder itemBuilder; + final int itemCount; + final String Function(int index) keyBuilder; + final double Function(int index) itemExtentBuilder; + final ReorderCallback onReorder; + final ReorderItemProxyDecorator? proxyDecorator; + + const CacheItemExtentSliverReorderableList({ + super.key, + required this.itemBuilder, + required this.keyBuilder, + required this.itemCount, + required this.itemExtentBuilder, + required this.onReorder, + this.proxyDecorator, + }); + + @override + State createState() => + CacheItemExtentSliverReorderableListState(); +} + +class CacheItemExtentSliverReorderableListState + extends State { + late final FixedMap _cacheHeightMap; + + @override + void initState() { + super.initState(); + _cacheHeightMap = FixedMap(widget.itemCount); + } + + clearCache() { + _cacheHeightMap.clear(); + } + + @override + Widget build(BuildContext context) { + _cacheHeightMap.updateMaxSize(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)); + }, + onReorder: widget.onReorder, + proxyDecorator: widget.proxyDecorator, + ); + } + + @override + void dispose() { + _cacheHeightMap.clear(); + super.dispose(); + } +} diff --git a/lib/widgets/open_container.dart b/lib/widgets/open_container.dart index f0dbfa2..e7ad6f8 100644 --- a/lib/widgets/open_container.dart +++ b/lib/widgets/open_container.dart @@ -82,6 +82,7 @@ class _OpenContainerState extends State> { child: GestureDetector( onTap: widget.tappable ? openContainer : null, child: Material( + color: Colors.transparent, clipBehavior: widget.clipBehavior, child: Builder( key: _closedBuilderKey, diff --git a/lib/widgets/pop_scope.dart b/lib/widgets/pop_scope.dart new file mode 100644 index 0000000..bc2dd06 --- /dev/null +++ b/lib/widgets/pop_scope.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; + +class CommonPopScope extends StatelessWidget { + final Widget child; + final FutureOr Function()? onPop; + + const CommonPopScope({ + super.key, + required this.child, + this.onPop, + }); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: onPop == null ? true : false, + onPopInvokedWithResult: onPop == null + ? null + : (didPop, __) async { + if (didPop) { + return; + } + final res = await onPop!(); + if (!context.mounted) { + return; + } + if (!res) { + return; + } + Navigator.of(context).pop(); + }, + child: child, + ); + } +} diff --git a/lib/widgets/popup.dart b/lib/widgets/popup.dart index e56f827..92e919b 100644 --- a/lib/widgets/popup.dart +++ b/lib/widgets/popup.dart @@ -40,39 +40,41 @@ class CommonPopupRoute extends PopupRoute { parent: animation, curve: Curves.easeIn, ).value; - return ValueListenableBuilder( - valueListenable: offsetNotifier, - builder: (_, value, child) { - return Align( - alignment: align, - child: CustomSingleChildLayout( - delegate: OverflowAwareLayoutDelegate( - offset: value.translate( - 48, - 12, - ), - ), - child: child, - ), - ); - }, - child: AnimatedBuilder( - animation: animation, - builder: (_, Widget? child) { - return Opacity( - opacity: 0.1 + 0.9 * animationValue, - child: Transform.scale( - alignment: align, - scale: 0.8 + 0.2 * animationValue, - child: Transform.translate( - offset: Offset(0, -10) * (1 - animationValue), - child: child!, + return SafeArea( + child: ValueListenableBuilder( + valueListenable: offsetNotifier, + builder: (_, value, child) { + return Align( + alignment: align, + child: CustomSingleChildLayout( + delegate: OverflowAwareLayoutDelegate( + offset: value.translate( + 48, + -8, + ), ), + child: child, ), ); }, - child: builder( - context, + child: AnimatedBuilder( + animation: animation, + builder: (_, Widget? child) { + return Opacity( + opacity: 0.1 + 0.9 * animationValue, + child: Transform.scale( + alignment: align, + scale: 0.8 + 0.2 * animationValue, + child: Transform.translate( + offset: Offset(0, -10) * (1 - animationValue), + child: child!, + ), + ), + ); + }, + child: builder( + context, + ), ), ), ); @@ -82,36 +84,47 @@ class CommonPopupRoute extends PopupRoute { Duration get transitionDuration => const Duration(milliseconds: 150); } +class PopupController extends ValueNotifier { + PopupController() : super(false); + + open() { + value = true; + } + + close() { + value = false; + } +} + +typedef PopupOpen = Function({ + Offset offset, +}); + class CommonPopupBox extends StatefulWidget { - final Widget target; + final Widget Function(PopupOpen open) targetBuilder; final Widget popup; const CommonPopupBox({ super.key, - required this.target, + required this.targetBuilder, required this.popup, }); @override - State createState() => CommonPopupBoxState(); + State createState() => _CommonPopupBoxState(); } -class CommonPopupBoxState extends State { - final _targetOffsetValueNotifier = ValueNotifier(Offset.zero); +class _CommonPopupBoxState extends State { + bool _isOpen = false; + final _targetOffsetValueNotifier = ValueNotifier(Offset.zero); + Offset _offset = Offset.zero; - _handleTargetOffset() { - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null) { - return; - } - _targetOffsetValueNotifier.value = renderBox.localToGlobal( - Offset.zero, - ); - } - - pop() { - _handleTargetOffset(); - Navigator.of(context).push( + _open({Offset offset = Offset.zero}) { + _offset = offset; + _updateOffset(); + _isOpen = true; + Navigator.of(context) + .push( CommonPopupRoute( barrierLabel: other.id, builder: (BuildContext context) { @@ -119,12 +132,38 @@ class CommonPopupBoxState extends State { }, offsetNotifier: _targetOffsetValueNotifier, ), - ); + ) + .then((_) { + _isOpen = false; + }); + } + + _updateOffset() { + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) { + return; + } + final viewPadding = MediaQuery.of(context).viewPadding; + _targetOffsetValueNotifier.value = renderBox + .localToGlobal( + Offset.zero.translate(viewPadding.right, viewPadding.top), + ) + .translate( + _offset.dx, + _offset.dy, + ); } @override Widget build(BuildContext context) { - return widget.target; + return LayoutBuilder(builder: (_, __) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_isOpen) { + _updateOffset(); + } + }); + return widget.targetBuilder(_open); + }); } } @@ -142,14 +181,14 @@ class OverflowAwareLayoutDelegate extends SingleChildLayoutDelegate { @override Offset getPositionForChild(Size size, Size childSize) { - final saveOffset = Offset(16, 16); + final safeOffset = Offset(16, 16); double x = (offset.dx - childSize.width).clamp( 0, - size.width - saveOffset.dx - childSize.width, + size.width - safeOffset.dx - childSize.width, ); double y = (offset.dy).clamp( 0, - size.height - saveOffset.dy - childSize.height, + size.height - safeOffset.dy - childSize.height, ); return Offset(x, y); } @@ -161,28 +200,41 @@ class OverflowAwareLayoutDelegate extends SingleChildLayoutDelegate { } class CommonPopupMenu extends StatelessWidget { - final List items; + final List items; + final double? minWidth; const CommonPopupMenu({ super.key, required this.items, + this.minWidth, }); Widget _popupMenuItem( BuildContext context, { - required ActionItemData item, + required PopupMenuItemData item, required int index, }) { - final isDanger = item.type == ActionType.danger; + final isDanger = item.type == PopupMenuItemType.danger; + final onPressed = item.onPressed; + final disabled = onPressed == null; final color = isDanger - ? context.colorScheme.error - : context.colorScheme.onSurfaceVariant; + ? disabled + ? context.colorScheme.error.opacity30 + : context.colorScheme.error + : disabled + ? context.colorScheme.onSurface.opacity30 + : context.colorScheme.onSurface; return InkWell( - onTap: () { - Navigator.of(context).pop(); - item.onPressed(); - }, - child: Padding( + onTap: onPressed != null + ? () { + Navigator.of(context).pop(); + onPressed(); + } + : null, + child: Container( + constraints: BoxConstraints( + minWidth: minWidth ?? 120, + ), padding: EdgeInsets.only( left: 16, right: 64, diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index a2ec22c..55d3440 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -1,31 +1,40 @@ 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:fl_clash/widgets/fade_box.dart'; +import 'package:fl_clash/widgets/pop_scope.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../enum/enum.dart'; import 'chip.dart'; class CommonScaffold extends StatefulWidget { + final PreferredSizeWidget? appBar; final Widget body; final Widget? bottomNavigationBar; final Widget? sideNavigationBar; - final String title; + final Color? backgroundColor; + final String? title; final Widget? leading; final List? actions; final bool automaticallyImplyLeading; + final bool? centerTitle; + final AppBarEditState? appBarEditState; const CommonScaffold({ super.key, + this.appBar, required this.body, this.sideNavigationBar, + this.backgroundColor, this.bottomNavigationBar, this.leading, - required this.title, + this.title, this.actions, this.automaticallyImplyLeading = true, + this.centerTitle, + this.appBarEditState, }); CommonScaffold.open({ @@ -51,8 +60,7 @@ class CommonScaffold extends StatefulWidget { } class CommonScaffoldState extends State { - final ValueNotifier _appBarState = - ValueNotifier(CommonAppBarState()); + late final ValueNotifier _appBarState; final ValueNotifier _floatingActionButton = ValueNotifier(null); final ValueNotifier> _keywordsNotifier = ValueNotifier([]); final ValueNotifier _loading = ValueNotifier(false); @@ -67,16 +75,46 @@ class CommonScaffoldState extends State { _appBarState.value = _appBarState.value.copyWith(actions: actions); } - set onSearch(Function(String)? onSearch) { - _appBarState.value = _appBarState.value.copyWith(onSearch: onSearch); + bool get _isSearch { + return _appBarState.value.searchState?.isSearch == true; + } + + bool get _isEdit { + return _appBarState.value.editState?.isEdit == true; } set onKeywordsUpdate(Function(List)? onKeywordsUpdate) { _onKeywordsUpdate = onKeywordsUpdate; } - set _searching(bool searching) { - _appBarState.value = _appBarState.value.copyWith(searching: searching); + @override + void initState() { + super.initState(); + _appBarState = ValueNotifier( + AppBarState( + editState: widget.appBarEditState, + ), + ); + } + + updateSearchState( + AppBarSearchState? Function(AppBarSearchState? state) builder, + ) { + _appBarState.value = _appBarState.value.copyWith( + searchState: builder( + _appBarState.value.searchState, + ), + ); + } + + updateEditState( + AppBarEditState? Function(AppBarEditState? state) builder, + ) { + _appBarState.value = _appBarState.value.copyWith( + editState: builder( + _appBarState.value.editState, + ), + ); } set floatingActionButton(Widget? floatingActionButton) { @@ -131,8 +169,8 @@ class CommonScaffoldState extends State { _handleClearInput() { _textController.text = ""; - if (_appBarState.value.onSearch != null) { - _appBarState.value.onSearch!(""); + if (_appBarState.value.searchState != null) { + _appBarState.value.searchState!.onSearch(""); } } @@ -141,12 +179,20 @@ class CommonScaffoldState extends State { _handleClearInput(); return; } - _searching = false; + updateSearchState( + (state) => state?.copyWith( + isSearch: false, + ), + ); } _handleExitSearching() { _handleClearInput(); - _searching = false; + updateSearchState( + (state) => state?.copyWith( + isSearch: false, + ), + ); } @override @@ -161,11 +207,15 @@ class CommonScaffoldState extends State { void didUpdateWidget(CommonScaffold oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.title != widget.title) { - _appBarState.value = CommonAppBarState(); + _appBarState.value = AppBarState(); _floatingActionButton.value = null; _textController.text = ""; _keywordsNotifier.value = []; _onKeywordsUpdate = null; + } else if (oldWidget.appBarEditState != widget.appBarEditState) { + _appBarState.value = _appBarState.value.copyWith( + editState: widget.appBarEditState, + ); } } @@ -184,8 +234,161 @@ class CommonScaffoldState extends State { _keywordsNotifier.value = keywords; } + Widget? _buildLeading() { + if (_isEdit) { + return IconButton( + onPressed: _appBarState.value.editState?.onExit, + icon: Icon(Icons.close), + ); + } + return _isSearch + ? IconButton( + onPressed: _handleExitSearching, + icon: Icon(Icons.arrow_back), + ) + : widget.leading; + } + + Widget _buildTitle(AppBarSearchState? startState) { + return _isSearch + ? TextField( + autofocus: true, + controller: _textController, + style: context.textTheme.titleLarge, + onChanged: (value) { + if (startState != null) { + startState.onSearch(value); + } + }, + decoration: InputDecoration( + hintText: appLocalizations.search, + ), + ) + : Text( + !_isEdit + ? widget.title! + : appLocalizations.selectedCountTitle( + "${_appBarState.value.editState?.editCount ?? 0}", + ), + ); + } + + List _buildActions( + bool hasSearch, + List actions, + ) { + if (_isSearch) { + return genActions([ + IconButton( + onPressed: _handleClear, + icon: Icon(Icons.close), + ), + ]); + } + return genActions( + [ + if (hasSearch) + IconButton( + onPressed: () { + updateSearchState( + (state) => state?.copyWith( + isSearch: true, + ), + ); + }, + icon: Icon(Icons.search), + ), + ...actions + ], + ); + } + + Widget _buildAppBarWrap(Widget appBar) { + if (_isEdit) { + return CommonPopScope( + onPop: () { + if (_isEdit) { + _appBarState.value.editState?.onExit(); + return false; + } + return true; + }, + child: appBar, + ); + } + return _isSearch + ? Theme( + data: _appBarTheme(context), + child: CommonPopScope( + onPop: () { + if (_isSearch) { + _handleExitSearching(); + return false; + } + return true; + }, + child: 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 ?? [], + ), + ), + ); + }, + ), + ValueListenableBuilder( + valueListenable: _loading, + builder: (_, value, __) { + return value == true + ? const LinearProgressIndicator() + : Container(); + }, + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { + assert(widget.appBar != null || widget.title != null); final body = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -228,121 +431,9 @@ class CommonScaffoldState extends State { ], ); final scaffold = Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - ValueListenableBuilder( - valueListenable: _appBarState, - builder: (_, state, __) { - final realActions = [ - if (state.onSearch != null) - IconButton( - onPressed: () { - _searching = true; - }, - icon: Icon(Icons.search), - ), - ...state.actions.isNotEmpty - ? state.actions - : widget.actions ?? [] - ]; - final appBar = AppBar( - 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: state.searching - ? IconButton( - onPressed: _handleExitSearching, - icon: Icon(Icons.arrow_back), - ) - : widget.leading, - title: state.searching - ? TextField( - autofocus: true, - controller: _textController, - style: context.textTheme.titleLarge, - onChanged: (value) { - if (state.onSearch != null) { - state.onSearch!(value); - } - }, - decoration: InputDecoration( - hintText: appLocalizations.search, - ), - ) - : Text(widget.title), - actions: [ - if (state.searching) - IconButton( - onPressed: _handleClear, - icon: Icon(Icons.close), - ) - else - Row( - children: [ - ...realActions.separated( - SizedBox( - width: 4, - ), - ) - ], - ), - SizedBox( - width: 8, - ) - ], - ); - return FadeBox( - child: state.searching - ? Theme( - data: _appBarTheme(context), - child: PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, __) { - if (didPop) { - return; - } - if (state.searching) { - _handleExitSearching(); - return; - } - Navigator.of(context).pop(); - }, - child: appBar, - ), - ) - : appBar, - ); - }, - ), - ValueListenableBuilder( - valueListenable: _loading, - builder: (_, value, __) { - return value == true - ? const LinearProgressIndicator() - : Container(); - }, - ), - ], - ), - ), + appBar: widget.appBar ?? _buildAppBar(), body: body, + backgroundColor: widget.backgroundColor, floatingActionButton: ValueListenableBuilder( valueListenable: _floatingActionButton, builder: (_, value, __) { @@ -367,3 +458,16 @@ class CommonScaffoldState extends State { : scaffold; } } + +List genActions(List actions, {double? space}) { + return [ + ...actions.separated( + SizedBox( + width: space ?? 4, + ), + ), + SizedBox( + width: 8, + ) + ]; +} diff --git a/lib/widgets/scroll.dart b/lib/widgets/scroll.dart index fc6abc8..ea9a7e4 100644 --- a/lib/widgets/scroll.dart +++ b/lib/widgets/scroll.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class CommonScrollBar extends StatelessWidget { - final ScrollController controller; + final ScrollController? controller; final Widget child; const CommonScrollBar({ @@ -23,3 +23,25 @@ class CommonScrollBar extends StatelessWidget { ); } } + +class CommonAutoHiddenScrollBar extends StatelessWidget { + final ScrollController? controller; + final Widget child; + + const CommonAutoHiddenScrollBar({ + super.key, + required this.child, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return Scrollbar( + controller: controller, + thickness: 8, + radius: const Radius.circular(8), + interactive: true, + child: child, + ); + } +} diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index 5abeb70..02c21d0 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -2,99 +2,184 @@ 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:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; +import 'scaffold.dart'; import 'side_sheet.dart'; -showExtendPage( - BuildContext context, { - required Widget body, - required String title, - double? extendPageWidth, - bool isScaffold = false, - bool isBlur = true, - Widget? action, -}) { - final NavigatorState navigator = Navigator.of(context); - final globalKey = GlobalKey(); - final uniqueBody = Container( - key: globalKey, - child: body, - ); - final isMobile = globalState.appState.viewMode == ViewMode.mobile; - if (isMobile) { - BaseNavigator.push( - context, - CommonScaffold( - title: title, - body: uniqueBody, - ), - ); - return; - } - final isNotSide = isMobile || isScaffold; - navigator.push( - ModalSideSheetRoute( - modalBarrierColor: Colors.black38, - builder: (context) { - final commonScaffold = CommonScaffold( - automaticallyImplyLeading: isNotSide, - actions: isNotSide - ? null - : [ - const SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: CloseButton(), - ), - ], - title: title, - body: uniqueBody, - ); - return SizedBox( - width: isMobile ? context.viewWidth : extendPageWidth ?? 300, - child: commonScaffold, - ); - }, - constraints: const BoxConstraints(), - filter: isBlur ? filter : null, - ), - ); +@immutable +class SheetProps { + final double? maxWidth; + final double? maxHeight; + final bool isScrollControlled; + final bool useSafeArea; + final bool blur; + + const SheetProps({ + this.maxWidth, + this.maxHeight, + this.useSafeArea = true, + this.isScrollControlled = false, + this.blur = true, + }); } +@immutable +class ExtendProps { + final double? maxWidth; + final bool useSafeArea; + final bool blur; + + const ExtendProps({ + this.maxWidth, + this.useSafeArea = true, + this.blur = true, + }); +} + +enum SheetType { + page, + bottomSheet, + sideSheet, +} + +typedef SheetBuilder = Widget Function(BuildContext context, SheetType type); + Future showSheet({ required BuildContext context, - required Widget body, - required String title, - bool isScrollControlled = true, - double width = 320, + required SheetBuilder builder, + SheetProps props = const SheetProps(), }) { final isMobile = globalState.appState.viewMode == ViewMode.mobile; - if (isMobile) { - return showModalBottomSheet( - context: context, - isScrollControlled: isScrollControlled, - builder: (context) { - return SafeArea( - child: body, - ); - }, - showDragHandle: true, - useSafeArea: true, + return switch (isMobile) { + true => showModalBottomSheet( + context: context, + isScrollControlled: props.isScrollControlled, + builder: (_) { + return SafeArea( + child: builder(context, SheetType.bottomSheet), + ); + }, + showDragHandle: false, + useSafeArea: props.useSafeArea, + ), + false => showModalSideSheet( + useSafeArea: props.useSafeArea, + isScrollControlled: props.isScrollControlled, + context: context, + constraints: BoxConstraints( + maxWidth: props.maxWidth ?? 360, + ), + filter: props.blur ? commonFilter : null, + builder: (_) { + return builder(context, SheetType.sideSheet); + }, + ), + }; +} + +Future showExtend( + BuildContext context, { + required SheetBuilder builder, + ExtendProps props = const ExtendProps(), +}) { + final isMobile = globalState.appState.viewMode == ViewMode.mobile; + return switch (isMobile) { + true => BaseNavigator.push( + context, + builder(context, SheetType.page), + ), + false => showModalSideSheet( + useSafeArea: props.useSafeArea, + context: context, + constraints: BoxConstraints( + maxWidth: props.maxWidth ?? 360, + ), + filter: props.blur ? commonFilter : null, + builder: (context) { + return builder(context, SheetType.sideSheet); + }, + ), + }; +} + +class AdaptiveSheetScaffold extends StatefulWidget { + final SheetType type; + final Widget body; + final String title; + final List actions; + + const AdaptiveSheetScaffold({ + super.key, + required this.type, + required this.body, + required this.title, + this.actions = const [], + }); + + @override + State createState() => _AdaptiveSheetScaffoldState(); +} + +class _AdaptiveSheetScaffoldState extends State { + @override + Widget build(BuildContext context) { + final backgroundColor = context.colorScheme.surface; + final bottomSheet = widget.type == SheetType.bottomSheet; + final sideSheet = widget.type == SheetType.sideSheet; + final appBar = AppBar( + forceMaterialTransparency: bottomSheet ? true : false, + automaticallyImplyLeading: bottomSheet + ? false + : widget.actions.isEmpty && sideSheet + ? false + : true, + centerTitle: bottomSheet, + backgroundColor: backgroundColor, + title: Text( + widget.title, + ), + actions: genActions([ + if (widget.actions.isEmpty && sideSheet) CloseButton(), + ...widget.actions, + ]), ); - } else { - return showModalSideSheet( - useSafeArea: true, - isScrollControlled: isScrollControlled, - context: context, - constraints: BoxConstraints( - maxWidth: width, - ), - body: SafeArea( - child: body, - ), - title: title, + if (widget.type == SheetType.bottomSheet) { + final handleSize = Size(32, 4); + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)), + color: backgroundColor, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only(top: 16), + child: Container( + alignment: Alignment.center, + height: handleSize.height, + width: handleSize.width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(handleSize.height / 2), + color: context.colorScheme.onSurfaceVariant, + ), + ), + ), + appBar, + Flexible( + flex: 1, + child: widget.body, + ) + ], + ), + ); + } + return CommonScaffold( + appBar: appBar, + backgroundColor: backgroundColor, + body: widget.body, ); } } diff --git a/lib/widgets/side_sheet.dart b/lib/widgets/side_sheet.dart index b132da7..f2a295d 100644 --- a/lib/widgets/side_sheet.dart +++ b/lib/widgets/side_sheet.dart @@ -1,3 +1,6 @@ +import 'dart:ui'; + +import 'package:fl_clash/common/color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -530,11 +533,11 @@ class ModalSideSheetRoute extends PopupRoute { @override Widget buildModalBarrier() { - if (barrierColor.alpha != 0 && !offstage) { - assert(barrierColor != barrierColor.withOpacity(0.0)); + if (barrierColor.a != 0 && !offstage) { + assert(barrierColor != barrierColor.opacity0); final Animation color = animation!.drive( ColorTween( - begin: barrierColor.withOpacity(0.0), + begin: barrierColor.opacity0, end: barrierColor, ).chain( CurveTween(curve: barrierCurve), @@ -562,8 +565,7 @@ class ModalSideSheetRoute extends PopupRoute { Future showModalSideSheet({ required BuildContext context, - required Widget body, - required String title, + required WidgetBuilder builder, Color? backgroundColor, String? barrierLabel, double? elevation, @@ -580,6 +582,7 @@ Future showModalSideSheet({ RouteSettings? routeSettings, AnimationController? transitionAnimationController, Offset? anchorPoint, + ImageFilter? filter, }) { assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMaterialLocalizations(context)); @@ -588,30 +591,8 @@ Future showModalSideSheet({ Navigator.of(context, rootNavigator: useRootNavigator); final MaterialLocalizations localizations = MaterialLocalizations.of(context); return navigator.push(ModalSideSheetRoute( - builder: (context) { - return SafeArea( - child: Column( - children: [ - AppBar( - automaticallyImplyLeading: false, - title: Text(title), - centerTitle: false, - actions: const [ - SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: CloseButton(), - ) - ], - ), - Expanded( - flex: 1, - child: body, - ), - ], - ), - ); - }, + builder: builder, + filter: filter, capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), isScrollControlled: isScrollControlled, @@ -633,3 +614,28 @@ Future showModalSideSheet({ useSafeArea: useSafeArea, )); } + +// class ModalAppBar extends StatelessWidget { +// final String title; +// +// const ModalAppBar({ +// super.key, +// required this.title, +// }); +// +// @override +// Widget build(BuildContext context) { +// return AppBar( +// automaticallyImplyLeading: false, +// title: Text(title), +// centerTitle: false, +// actions: const [ +// SizedBox( +// height: kToolbarHeight, +// width: kToolbarHeight, +// child: CloseButton(), +// ) +// ], +// ); +// } +// } diff --git a/lib/widgets/subscription_info_view.dart b/lib/widgets/subscription_info_view.dart index c7c43a9..5dc0052 100644 --- a/lib/widgets/subscription_info_view.dart +++ b/lib/widgets/subscription_info_view.dart @@ -35,7 +35,7 @@ class SubscriptionInfoView extends StatelessWidget { LinearProgressIndicator( minHeight: 6, value: progress, - backgroundColor: context.colorScheme.primary.toSoft, + backgroundColor: context.colorScheme.primary.opacity15, ), const SizedBox( height: 8, diff --git a/lib/widgets/super_grid.dart b/lib/widgets/super_grid.dart index 563c378..a8336fd 100644 --- a/lib/widgets/super_grid.dart +++ b/lib/widgets/super_grid.dart @@ -99,23 +99,27 @@ class SuperGridState extends State with TickerProviderStateMixin { return; } showSheet( - width: 360, + builder: (_, type) { + return ValueListenableBuilder( + valueListenable: addedChildrenNotifier, + builder: (_, value, __) { + return AdaptiveSheetScaffold( + type: type, + body: _AddedWidgetsModal( + items: value, + onAdd: (gridItem) { + _childrenNotifier.value = List.from(_childrenNotifier.value) + ..add( + gridItem, + ); + }, + ), + title: appLocalizations.add, + ); + }, + ); + }, context: context, - body: ValueListenableBuilder( - valueListenable: addedChildrenNotifier, - builder: (_, value, __) { - return _AddedWidgetsModal( - items: value, - onAdd: (gridItem) { - _childrenNotifier.value = List.from(_childrenNotifier.value) - ..add( - gridItem, - ); - }, - ); - }, - ), - title: appLocalizations.add, ); } @@ -662,7 +666,8 @@ 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/widgets.dart b/lib/widgets/widgets.dart index 05df389..22ccf6c 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,5 +1,5 @@ export 'animate_grid.dart'; -export 'back_scope.dart'; +export 'pop_scope.dart'; export 'builder.dart'; export 'card.dart'; export 'chip.dart'; @@ -27,3 +27,5 @@ export 'donut_chart.dart'; export 'activate_box.dart'; export 'wave.dart'; export 'scroll.dart'; +export 'dialog.dart'; +export 'effect.dart'; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 40b439d..9fe12f3 100755 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -99,9 +99,9 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 + app_links: afe860c55c7ef176cea7fb630a2b7d7736de591d connectivity_plus: 2256d3e20624a7749ed21653aafe291a46446fee - device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 + device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 dynamic_color: b820c000cc68df65e7ba7ff177cb98404ce56651 file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 295c4ca..0a74486 100755 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 7AC6855B2B8AF836004C123B /* (null) in Bundle Framework */ = {isa = PBXBuildFile; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CDD319C761C7664F6008596B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */; }; F50091052CF74B7700D43AEA /* FlClashCore in CopyFiles */ = {isa = PBXBuildFile; fileRef = F50091042CF74B7700D43AEA /* FlClashCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + F5AA39AF2DA1D9FB00F5C816 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = F5AA39AE2DA1D9FB00F5C816 /* LaunchAtLogin */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -135,6 +136,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F5AA39AF2DA1D9FB00F5C816 /* LaunchAtLogin in Frameworks */, CDD319C761C7664F6008596B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -326,6 +328,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + F5AA39AD2DA1D9FB00F5C816 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -827,6 +832,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + F5AA39AD2DA1D9FB00F5C816 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + F5AA39AE2DA1D9FB00F5C816 /* LaunchAtLogin */ = { + isa = XCSwiftPackageProductDependency; + package = F5AA39AD2DA1D9FB00F5C816 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */; + productName = LaunchAtLogin; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 0a58fd4..aee339e 100755 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -1,19 +1,37 @@ import Cocoa import FlutterMacOS import window_manager +import LaunchAtLogin class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - super.awakeFromNib() - } - override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { - super.order(place, relativeTo: otherWin) - hiddenWindowAtLaunch() - } + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + FlutterMethodChannel( + name: "launch_at_startup", binaryMessenger: flutterViewController.engine.binaryMessenger + ) + .setMethodCallHandler { (_ call: FlutterMethodCall, result: @escaping FlutterResult) in + switch call.method { + case "launchAtStartupIsEnabled": + result(LaunchAtLogin.isEnabled) + case "launchAtStartupSetEnabled": + if let arguments = call.arguments as? [String: Any] { + LaunchAtLogin.isEnabled = arguments["setEnabledValue"] as! Bool + } + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + + RegisterGeneratedPlugins(registry: flutterViewController) + super.awakeFromNib() + } + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } } diff --git a/plugins/flutter_distributor b/plugins/flutter_distributor index 7e7bcad..44a8396 160000 --- a/plugins/flutter_distributor +++ b/plugins/flutter_distributor @@ -1 +1 @@ -Subproject commit 7e7bcadf2909bc8acf77a0ba5ae056bea9a60567 +Subproject commit 44a8396d30bbc21348f8e499f4340765dbcb564b diff --git a/pubspec.lock b/pubspec.lock index d17caa1..f84e560 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,10 +42,34 @@ packages: dependency: "direct main" description: name: app_links - sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: "direct main" description: @@ -322,10 +346,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513" url: "https://pub.dev" source: hosted - version: "10.1.2" + version: "11.3.3" device_info_plus_platform_interface: dependency: transitive description: @@ -386,10 +410,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" ffigen: dependency: "direct dev" description: @@ -538,14 +562,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 - url: "https://pub.dev" - source: hosted - version: "6.2.1" graphs: dependency: transitive description: @@ -758,10 +774,10 @@ packages: dependency: "direct main" description: name: launch_at_startup - sha256: "93fc5638e088290004fae358bae691486673d469957d461d9dae5b12248593eb" + sha256: "7db33398b76ec0ed9e27f9f4640553e239977437564046625e215be89c91f084" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.5.1" leak_tracker: dependency: transitive description: @@ -994,14 +1010,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process_run: - dependency: "direct main" - description: - name: process_run - sha256: a68fa9727392edad97a2a96a77ce8b0c17d28336ba1b284b1dfac9595a4299ea - url: "https://pub.dev" - source: hosted - version: "1.2.2+1" proxy: dependency: "direct main" description: @@ -1037,10 +1045,10 @@ packages: dependency: "direct main" description: name: re_editor - sha256: "2169c114c7877bcaae72d6e8b69cdaa2a9cded69a51e3cf26209dad4a3ed2b9c" + sha256: "17e430f0591dd361992ec2dd6f69191c1853fa46e05432e095310a8f82ee820e" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" re_highlight: dependency: "direct main" description: @@ -1370,10 +1378,10 @@ packages: dependency: "direct main" description: name: tray_manager - sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" + sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.0" typed_data: dependency: transitive description: @@ -1522,18 +1530,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.12.0" win32_registry: dependency: "direct main" description: name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "2.1.0" window_ext: dependency: "direct main" description: @@ -1590,5 +1598,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 738c439..eb9c22c 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.80+202503101 +version: 0.8.81+202504081 environment: sdk: '>=3.1.0 <4.0.0' @@ -20,14 +20,14 @@ dependencies: path: plugins/proxy window_ext: path: plugins/window_ext - launch_at_startup: ^0.2.2 + launch_at_startup: ^0.5.1 windows_single_instance: ^1.0.1 json_annotation: ^4.9.0 file_picker: ^8.0.3 mobile_scanner: ^6.0.2 - app_links: ^3.5.0 - win32_registry: ^1.1.5 - tray_manager: ^0.3.2 + app_links: ^6.4.0 + win32_registry: ^2.0.0 + tray_manager: ^0.4.0 collection: ^1.18.0 animations: ^2.0.11 package_info_plus: ^8.0.0 @@ -38,23 +38,21 @@ dependencies: dio: ^5.8.0+1 win32: ^5.5.1 ffi: ^2.1.2 - re_editor: ^0.6.0 + re_editor: ^0.7.0 re_highlight: ^0.0.3 archive: ^3.6.1 lpinyin: ^2.0.3 emoji_regex: ^0.0.5 - process_run: ^1.1.0 cached_network_image: ^3.4.0 hotkey_manager: ^0.2.3 uni_platform: ^0.1.3 - device_info_plus: ^10.1.2 + device_info_plus: ^11.3.3 connectivity_plus: ^6.1.0 screen_retriever: ^0.2.0 defer_pointer: ^0.0.2 flutter_riverpod: ^2.6.1 riverpod_annotation: ^2.6.1 riverpod: ^2.6.1 - google_fonts: ^6.2.1 dev_dependencies: flutter_test: sdk: flutter @@ -76,6 +74,9 @@ flutter: - assets/images/ - assets/images/avatars/ fonts: + - family: JetBrainsMono + fonts: + - asset: assets/fonts/JetBrainsMono-Regular.ttf - family: Twemoji fonts: - asset: assets/fonts/Twemoji.Mozilla.ttf diff --git a/release.py b/release_telegram.py similarity index 96% rename from release.py rename to release_telegram.py index f540804..4e4c253 100644 --- a/release.py +++ b/release_telegram.py @@ -5,7 +5,7 @@ import requests TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") TAG = os.getenv("TAG") -IS_RELEASE = "+" not in TAG +IS_STABLE = "-" not in TAG CHAT_ID = "@FlClash" API_URL = f"http://localhost:8081/bot{TELEGRAM_BOT_TOKEN}/sendMediaGroup" @@ -33,7 +33,7 @@ for file in os.listdir(DIST_DIR): if TAG: text += f"\n**{TAG}**\n" -if IS_RELEASE: +if IS_STABLE: text += f"\nhttps://github.com/chen08209/FlClash/releases/tag/{TAG}\n" diff --git a/setup.dart b/setup.dart index 801ce13..8c5f305 100755 --- a/setup.dart +++ b/setup.dart @@ -358,6 +358,14 @@ class BuildCommand extends Command { ].join(','), help: 'The $name build arch', ); + argParser.addOption( + "env", + valueHelp: [ + "pre", + "stable", + ].join(','), + help: 'The $name build env', + ); } @override @@ -423,12 +431,13 @@ class BuildCommand extends Command { required Target target, required String targets, String args = '', + required String env, }) async { await Build.getDistributor(); await Build.exec( name: name, Build.getExecutable( - "flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose $args", + "flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose $args --build-dart-define=APP_ENV=$env", ), ); } @@ -448,6 +457,7 @@ class BuildCommand extends Command { final mode = target == Target.android ? Mode.lib : Mode.core; final String out = argResults?["out"] ?? (target.same ? "app" : "core"); final archName = argResults?["arch"]; + final env = argResults?["env"] ?? "pre"; final currentArches = arches.where((element) => element.name == archName).toList(); final arch = currentArches.isEmpty ? null : currentArches.first; @@ -476,6 +486,7 @@ class BuildCommand extends Command { target: target, targets: "exe,zip", args: "--description $archName", + env: env, ); return; case Target.linux: @@ -497,6 +508,7 @@ class BuildCommand extends Command { targets: targets, args: "--description $archName --build-target-platform $defaultTarget", + env: env, ); return; case Target.android: @@ -515,6 +527,7 @@ class BuildCommand extends Command { targets: "apk", args: "--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}", + env: env, ); return; case Target.macos: @@ -523,6 +536,7 @@ class BuildCommand extends Command { target: target, targets: "dmg", args: "--description $archName", + env: env, ); return; }