From c9cd80bcb32376049b380fe5b6933a34d76343b3 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Wed, 9 Apr 2025 16:46:14 +0800 Subject: [PATCH] Optimize android vpn performance Add custom primary color and color scheme Add linux nad windows arm release Optimize requests and logs page --- .github/workflows/build.yaml | 35 +- .gitmodules | 4 - Makefile | 10 + android/app/build.gradle | 31 +- android/app/src/debug/AndroidManifest.xml | 16 +- .../kotlin/com/follow/clash/MainActivity.kt | 1 + .../com/follow/clash/plugins/ServicePlugin.kt | 1 - .../com/follow/clash/plugins/VpnPlugin.kt | 118 ++--- android/build.gradle | 1 + android/core/.gitignore | 1 + android/core/build.gradle.kts | 65 +++ android/core/consumer-rules.pro | 0 android/core/proguard-rules.pro | 21 + android/core/src/main/AndroidManifest.xml | 4 + android/core/src/main/cpp/CMakeLists.txt | 45 ++ android/core/src/main/cpp/core.cpp | 75 +++ android/core/src/main/cpp/jni_helper.cpp | 70 +++ android/core/src/main/cpp/jni_helper.h | 39 ++ .../main/java/com/follow/clash/core/Core.kt | 50 ++ .../com/follow/clash/core/TunInterface.kt | 9 + android/settings.gradle | 1 + core/action.go | 4 +- core/android_bride.go | 77 +++ core/common.go | 2 +- core/constant.go | 26 +- core/hub.go | 10 +- core/lib_android.go | 274 ++++------- core/platform/procfs.go | 176 +++++++ lib/application.dart | 16 +- lib/clash/core.dart | 8 +- lib/clash/generated/clash_ffi.dart | 194 ++++++-- lib/clash/interface.dart | 12 +- lib/clash/lib.dart | 71 +-- lib/common/color.dart | 57 ++- lib/common/common.dart | 2 +- lib/common/constant.dart | 12 +- lib/common/launch.dart | 4 + lib/common/render.dart | 2 +- lib/common/request.dart | 2 +- lib/common/tray.dart | 4 +- lib/common/{other.dart => utils.dart} | 61 ++- lib/controller.dart | 29 +- lib/fragments/backup_and_recovery.dart | 2 +- lib/fragments/config/config.dart | 1 - lib/fragments/connection/requests.dart | 22 +- .../dashboard/widgets/start_button.dart | 10 +- lib/fragments/logs.dart | 20 +- lib/fragments/profiles/override_profile.dart | 1 + lib/fragments/proxies/card.dart | 2 +- lib/fragments/theme.dart | 443 +++++++++++++----- lib/fragments/tools.dart | 2 +- lib/l10n/arb/intl_en.arb | 15 +- lib/l10n/arb/intl_ja.arb | 15 +- lib/l10n/arb/intl_ru.arb | 15 +- lib/l10n/arb/intl_zh_CN.arb | 15 +- lib/l10n/intl/messages_en.dart | 17 + lib/l10n/intl/messages_ja.dart | 13 + lib/l10n/intl/messages_ru.dart | 17 + lib/l10n/intl/messages_zh_CN.dart | 13 + lib/l10n/l10n.dart | 100 ++++ lib/main.dart | 53 +-- lib/manager/app_state_manager.dart | 13 +- lib/manager/message_manager.dart | 4 +- lib/manager/window_manager.dart | 9 +- lib/models/app.dart | 3 +- lib/models/clash_config.dart | 6 +- lib/models/common.dart | 21 +- lib/models/config.dart | 33 +- lib/models/core.dart | 11 + lib/models/generated/app.freezed.dart | 76 ++- .../generated/clash_config.freezed.dart | 20 +- lib/models/generated/clash_config.g.dart | 4 +- lib/models/generated/config.freezed.dart | 94 +++- lib/models/generated/config.g.dart | 33 +- lib/models/generated/core.freezed.dart | 172 +++++++ lib/models/generated/core.g.dart | 12 + lib/models/profile.dart | 2 +- lib/models/selector.dart | 8 +- lib/plugins/service.dart | 1 - lib/plugins/vpn.dart | 16 - lib/providers/app.dart | 19 +- lib/providers/config.dart | 2 +- lib/providers/generated/app.g.dart | 18 +- lib/providers/generated/config.g.dart | 2 +- lib/providers/generated/state.g.dart | 167 ++++++- lib/providers/state.dart | 34 +- lib/state.dart | 4 +- lib/widgets/card.dart | 2 +- lib/widgets/color_scheme_box.dart | 55 ++- lib/widgets/list.dart | 134 +----- lib/widgets/palette.dart | 442 +++++++++++++++++ lib/widgets/popup.dart | 2 +- lib/widgets/scaffold.dart | 2 + lib/widgets/scroll.dart | 198 ++++++++ lib/widgets/widgets.dart | 1 + macos/Runner.xcodeproj/project.pbxproj | 2 +- plugins/flutter_distributor | 2 +- pubspec.yaml | 2 +- release_telegram.py | 26 +- setup.dart | 10 +- 100 files changed, 3081 insertions(+), 997 deletions(-) create mode 100644 Makefile create mode 100644 android/core/.gitignore create mode 100644 android/core/build.gradle.kts create mode 100644 android/core/consumer-rules.pro create mode 100644 android/core/proguard-rules.pro create mode 100644 android/core/src/main/AndroidManifest.xml create mode 100644 android/core/src/main/cpp/CMakeLists.txt create mode 100644 android/core/src/main/cpp/core.cpp create mode 100644 android/core/src/main/cpp/jni_helper.cpp create mode 100644 android/core/src/main/cpp/jni_helper.h create mode 100644 android/core/src/main/java/com/follow/clash/core/Core.kt create mode 100644 android/core/src/main/java/com/follow/clash/core/TunInterface.kt create mode 100644 core/android_bride.go create mode 100644 core/platform/procfs.go rename lib/common/{other.dart => utils.dart} (82%) create mode 100644 lib/widgets/palette.dart diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0b8db24..e7fda7f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,29 +27,27 @@ jobs: - platform: macos os: macos-latest arch: arm64 + - platform: windows + os: windows-11-arm + arch: arm64 + - platform: linux + os: ubuntu-24.04-arm + arch: arm64 steps: + - name: Setup rust + if: startsWith(matrix.os, 'windows-11-arm') + run: | + Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe + .\rustup-init.exe -y --default-toolchain stable + $cargoPath = "$env:USERPROFILE\.cargo\bin" + Add-Content $env:GITHUB_PATH $cargoPath + - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - - name: Setup JAVA - if: startsWith(matrix.platform,'android') - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: 17 - - - name: Setup NDK - if: startsWith(matrix.platform,'android') - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: r26b - add-to-path: true - link-to-sdk: true - - name: Setup Android Signing if: startsWith(matrix.platform,'android') run: | @@ -58,18 +56,17 @@ jobs: echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties - - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 'stable' + go-version: '1.24.0' cache-dependency-path: | core/go.sum - name: Setup Flutter uses: subosito/flutter-action@v2 with: - channel: stable + channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }} cache: true - name: Get Flutter Dependency diff --git a/.gitmodules b/.gitmodules index 70bf7cd..cfcef0a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,9 +6,5 @@ path = plugins/flutter_distributor url = git@github.com:chen08209/flutter_distributor.git branch = FlClash -[submodule "plugins/tray_manager"] - path = plugins/tray_manager - url = git@github.com:chen08209/tray_manager.git - branch = main diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..627966f --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +android_arm64: + dart ./setup.dart android --arch arm64 +macos_arm64: + dart ./setup.dart macos --arch arm64 +android_app: + dart ./setup.dart android +android_arm64_core: + dart ./setup.dart android --arch arm64 --out core +macos_arm64_core: + dart ./setup.dart macos --arch arm64 --out core \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index bc8d1d0..ab0a450 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,3 @@ -import com.android.build.gradle.tasks.MergeSourceSetFolders - plugins { id "com.android.application" id "kotlin-android" @@ -33,8 +31,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias android { namespace "com.follow.clash" - compileSdkVersion 35 - ndkVersion "27.1.12297006" + compileSdk 35 + ndkVersion = "28.0.13004108" compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -48,6 +46,7 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' } + signingConfigs { if (isRelease) { release { @@ -84,31 +83,15 @@ android { } } -tasks.register('copyNativeLibs', Copy) { - delete('src/main/jniLibs') - from('../../libclash/android') - into('src/main/jniLibs') -} - -tasks.withType(MergeSourceSetFolders).configureEach { - dependsOn copyNativeLibs -} - flutter { source '../..' } dependencies { + implementation project(":core") implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'com.google.code.gson:gson:2.10' - implementation("com.android.tools.smali:smali-dexlib2:3.0.7") { + implementation 'com.google.code.gson:gson:2.10.1' + implementation("com.android.tools.smali:smali-dexlib2:3.0.9") { exclude group: "com.google.guava", module: "guava" } -} - - -afterEvaluate { - assembleDebug.dependsOn copyNativeLibs - - assembleRelease.dependsOn copyNativeLibs -} +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 74ddb95..8fe3c0a 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,13 +1,17 @@ - + - + - + android:name=".services.FlClashTileService" + android:label="FlClash Debug" + tools:replace="android:label" + tools:targetApi="24" /> diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt index c404697..6e186da 100644 --- a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt @@ -1,5 +1,6 @@ package com.follow.clash +import com.follow.clash.core.Core import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.TilePlugin diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt index 2f7ef69..d0f3010 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 @@ -52,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } private fun handleDestroy() { - GlobalState.getCurrentVPNPlugin()?.handleStop() GlobalState.destroyServiceEngine() } } \ No newline at end of file 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 c4f6e7e..e316110 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 @@ -14,10 +14,9 @@ import androidx.core.content.getSystemService import com.follow.clash.FlClashApplication import com.follow.clash.GlobalState import com.follow.clash.RunState +import com.follow.clash.core.Core import com.follow.clash.extensions.awaitResult -import com.follow.clash.extensions.getProtocol import com.follow.clash.extensions.resolveDns -import com.follow.clash.models.Process import com.follow.clash.models.StartForegroundParams import com.follow.clash.models.VpnOptions import com.follow.clash.services.BaseServiceInterface @@ -40,10 +39,12 @@ import kotlin.concurrent.withLock data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private lateinit var flutterMethodChannel: MethodChannel private var flClashService: BaseServiceInterface? = null - private lateinit var options: VpnOptions + private var options: VpnOptions? = null + private var isBind: Boolean = false private lateinit var scope: CoroutineScope private var lastStartForegroundParams: StartForegroundParams? = null private var timerJob: Job? = null + private val uidPageNameMap = mutableMapOf() private val connectivity by lazy { FlClashApplication.getAppContext().getSystemService() @@ -51,6 +52,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { + isBind = true flClashService = when (service) { is FlClashVpnService.LocalBinder -> service.getService() is FlClashService.LocalBinder -> service.getService() @@ -60,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } override fun onServiceDisconnected(arg: ComponentName) { + isBind = false flClashService = null } } @@ -90,62 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { result.success(true) } - "setProtect" -> { - val fd = call.argument("fd") - if (fd != null && flClashService is FlClashVpnService) { - try { - (flClashService as FlClashVpnService).protect(fd) - result.success(true) - } catch (e: RuntimeException) { - result.success(false) - } - } else { - result.success(false) - } - } - - "resolverProcess" -> { - val data = call.argument("data") - val process = if (data != null) Gson().fromJson( - data, Process::class.java - ) else null - val metadata = process?.metadata - if (metadata == null) { - result.success(null) - return - } - val protocol = metadata.getProtocol() - if (protocol == null) { - result.success(null) - return - } - scope.launch { - withContext(Dispatchers.Default) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - result.success(null) - return@withContext - } - val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort) - val dst = InetSocketAddress( - metadata.destinationIP.ifEmpty { metadata.host }, - metadata.destinationPort - ) - val uid = try { - connectivity?.getConnectionOwnerUid(protocol, src, dst) - } catch (_: Exception) { - null - } - if (uid == null || uid == -1) { - result.success(null) - return@withContext - } - val packages = - FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid) - result.success(packages?.first()) - } - } - } - else -> { result.notImplemented() } @@ -153,6 +100,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } fun handleStart(options: VpnOptions): Boolean { + if (options.enable != this.options?.enable) { + this.flClashService = null + } this.options = options when (options.enable) { true -> handleStartVpn() @@ -162,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } private fun handleStartVpn() { - GlobalState.getCurrentAppPlugin() - ?.requestVpnPermission { - handleStartService() - } + GlobalState.getCurrentAppPlugin()?.requestVpnPermission { + handleStartService() + } } fun requestGc() { @@ -235,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } private fun startForegroundJob() { + stopForegroundJob() timerJob = CoroutineScope(Dispatchers.Main).launch { while (isActive) { startForeground() @@ -256,26 +206,58 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { GlobalState.runLock.withLock { if (GlobalState.runState.value == RunState.START) return GlobalState.runState.value = RunState.START - val fd = flClashService?.start(options) - flutterMethodChannel.invokeMethod( - "started", fd + val fd = flClashService?.start(options!!) + Core.startTun( + fd = fd ?: 0, + protect = this::protect, + resolverProcess = this::resolverProcess, ) - startForegroundJob(); + startForegroundJob() } } + private fun protect(fd: Int): Boolean { + return (flClashService as? FlClashVpnService)?.protect(fd) == true + } + + private fun resolverProcess( + protocol: Int, + source: InetSocketAddress, + target: InetSocketAddress, + uid: Int, + ): String { + val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1 + } else { + uid + } + if (nextUid == -1) { + return "" + } + if (!uidPageNameMap.containsKey(nextUid)) { + uidPageNameMap[nextUid] = + FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid) + ?.first() ?: "" + } + return uidPageNameMap[nextUid] ?: "" + } + fun handleStop() { GlobalState.runLock.withLock { if (GlobalState.runState.value == RunState.STOP) return GlobalState.runState.value = RunState.STOP stopForegroundJob() + Core.stopTun() flClashService?.stop() GlobalState.handleTryDestroy() } } private fun bindService() { - val intent = when (options.enable) { + if (isBind) { + FlClashApplication.getAppContext().unbindService(connection) + } + val intent = when (options?.enable == true) { true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java) false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java) } diff --git a/android/build.gradle b/android/build.gradle index 8feab38..6c317e4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -24,6 +24,7 @@ subprojects { } subprojects { project.evaluationDependsOn(':app') + project.evaluationDependsOn(':core') } tasks.register("clean", Delete) { diff --git a/android/core/.gitignore b/android/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/android/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/core/build.gradle.kts b/android/core/build.gradle.kts new file mode 100644 index 0000000..63c5e41 --- /dev/null +++ b/android/core/build.gradle.kts @@ -0,0 +1,65 @@ +import com.android.build.gradle.tasks.MergeSourceSetFolders + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.follow.clash.core" + compileSdk = 35 + ndkVersion = "28.0.13004108" + + defaultConfig { + minSdk = 21 + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + sourceSets { + getByName("main") { + jniLibs.srcDirs("src/main/jniLibs") + } + } + + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +val copyNativeLibs by tasks.register("copyNativeLibs") { + doFirst { + delete("src/main/jniLibs") + } + from("../../libclash/android") + into("src/main/jniLibs") +} + +afterEvaluate { + tasks.named("preBuild") { + dependsOn(copyNativeLibs) + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.16.0") +} \ No newline at end of file diff --git a/android/core/consumer-rules.pro b/android/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/android/core/proguard-rules.pro b/android/core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/android/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/core/src/main/AndroidManifest.xml b/android/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/android/core/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/core/src/main/cpp/CMakeLists.txt b/android/core/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..0246d86 --- /dev/null +++ b/android/core/src/main/cpp/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.22.1) + +project("core") + +message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}") + +if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + add_compile_options(-O3) + + add_compile_options(-flto) + + add_compile_options(-g0) + + add_compile_options(-ffunction-sections -fdata-sections) + + add_compile_options(-fno-exceptions -fno-rtti) + + add_link_options( + -flto + -Wl,--gc-sections + -Wl,--strip-all + -Wl,--exclude-libs=ALL + ) +endif () + +set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so") + +message("LIB_CLASH_PATH ${LIB_CLASH_PATH}") +if (EXISTS ${LIB_CLASH_PATH}) + message("Found libclash.so for ABI ${ANDROID_ABI}") + add_compile_definitions(LIBCLASH) + include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) + link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}) + add_library(${CMAKE_PROJECT_NAME} SHARED + jni_helper.cpp + core.cpp) + target_link_libraries(${CMAKE_PROJECT_NAME} + clash) +else () + message("Not found libclash.so for ABI ${ANDROID_ABI}") + add_library(${CMAKE_PROJECT_NAME} SHARED + jni_helper.cpp + core.cpp) + target_link_libraries(${CMAKE_PROJECT_NAME}) +endif () \ No newline at end of file diff --git a/android/core/src/main/cpp/core.cpp b/android/core/src/main/cpp/core.cpp new file mode 100644 index 0000000..5392300 --- /dev/null +++ b/android/core/src/main/cpp/core.cpp @@ -0,0 +1,75 @@ +#include + +#ifdef LIBCLASH +#include +#include +#include "jni_helper.h" +#include "libclash.h" + +extern "C" +JNIEXPORT void JNICALL +Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) { + auto interface = new_global(cb); + startTUN(fd, interface); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) { + stopTun(); +} + + +static jmethodID m_tun_interface_protect; +static jmethodID m_tun_interface_resolve_process; + + +static void release_jni_object_impl(void *obj) { + ATTACH_JNI(); + del_global((jobject) obj); +} + +static void call_tun_interface_protect_impl(void *tun_interface, int fd) { + ATTACH_JNI(); + env->CallVoidMethod((jobject) tun_interface, + (jmethodID) m_tun_interface_protect, + (jint) fd); +} + +static const char* +call_tun_interface_resolve_process_impl(void *tun_interface, int protocol, + const char *source, + const char *target, + int uid) { + ATTACH_JNI(); + jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface, + (jmethodID) m_tun_interface_resolve_process, + (jint) protocol, + (jstring) new_string(source), + (jstring) new_string(target), + (jint) uid); + return get_string(packageName); +} + +extern "C" +JNIEXPORT jint JNICALL +JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIEnv *env = nullptr; + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + + initialize_jni(vm, env); + + jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface"); + + m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V"); + m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess", + "(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;"); + + registerCallbacks(&call_tun_interface_protect_impl, + &call_tun_interface_resolve_process_impl, + &release_jni_object_impl); + return JNI_VERSION_1_6; +} +#endif \ No newline at end of file diff --git a/android/core/src/main/cpp/jni_helper.cpp b/android/core/src/main/cpp/jni_helper.cpp new file mode 100644 index 0000000..cb0f853 --- /dev/null +++ b/android/core/src/main/cpp/jni_helper.cpp @@ -0,0 +1,70 @@ +#include "jni_helper.h" + +#include +#include + +static JavaVM *global_vm; + +static jclass c_string; +static jmethodID m_new_string; +static jmethodID m_get_bytes; + +void initialize_jni(JavaVM *vm, JNIEnv *env) { + global_vm = vm; + + c_string = (jclass) new_global(find_class("java/lang/String")); + m_new_string = find_method(c_string, "", "([B)V"); + m_get_bytes = find_method(c_string, "getBytes", "()[B"); +} + +JavaVM *global_java_vm() { + return global_vm; +} + +char *jni_get_string(JNIEnv *env, jstring str) { + auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes); + int length = env->GetArrayLength(array); + char *content = (char *) malloc(length + 1); + env->GetByteArrayRegion(array, 0, length, (jbyte *) content); + content[length] = 0; + return content; +} + +jstring jni_new_string(JNIEnv *env, const char *str) { + auto length = (int) strlen(str); + jbyteArray array = env->NewByteArray(length); + env->SetByteArrayRegion(array, 0, length, (const jbyte *) str); + return (jstring) env->NewObject(c_string, m_new_string, array); +} + +int jni_catch_exception(JNIEnv *env) { + int result = env->ExceptionCheck(); + if (result) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + return result; +} + +void jni_attach_thread(struct scoped_jni *jni) { + JavaVM *vm = global_java_vm(); + if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) { + jni->require_release = 0; + return; + } + if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) { + abort(); + } + jni->require_release = 1; +} + +void jni_detach_thread(struct scoped_jni *jni) { + JavaVM *vm = global_java_vm(); + if (jni->require_release) { + vm->DetachCurrentThread(); + } +} + +void release_string(char **str) { + free(*str); +} \ No newline at end of file diff --git a/android/core/src/main/cpp/jni_helper.h b/android/core/src/main/cpp/jni_helper.h new file mode 100644 index 0000000..a952865 --- /dev/null +++ b/android/core/src/main/cpp/jni_helper.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +struct scoped_jni { + JNIEnv *env; + int require_release; +}; + +extern void initialize_jni(JavaVM *vm, JNIEnv *env); + +extern jstring jni_new_string(JNIEnv *env, const char *str); + +extern char *jni_get_string(JNIEnv *env, jstring str); + +extern int jni_catch_exception(JNIEnv *env); + +extern void jni_attach_thread(struct scoped_jni *jni); + +extern void jni_detach_thread(struct scoped_jni *env); + +extern void release_string(char **str); + +#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \ + struct scoped_jni _jni; \ + jni_attach_thread(&_jni); \ + JNIEnv *env = _jni.env + +#define scoped_string __attribute__((cleanup(release_string))) char* + +#define find_class(name) env->FindClass(name) +#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature) +#define new_global(obj) env->NewGlobalRef(obj) +#define del_global(obj) env->DeleteGlobalRef(obj) +#define get_string(jstr) jni_get_string(env, jstr) +#define new_string(cstr) jni_new_string(env, cstr) \ No newline at end of file diff --git a/android/core/src/main/java/com/follow/clash/core/Core.kt b/android/core/src/main/java/com/follow/clash/core/Core.kt new file mode 100644 index 0000000..c863cae --- /dev/null +++ b/android/core/src/main/java/com/follow/clash/core/Core.kt @@ -0,0 +1,50 @@ +package com.follow.clash.core + +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.URL + +data object Core { + private external fun startTun( + fd: Int, + cb: TunInterface + ) + + private fun parseInetSocketAddress(address: String): InetSocketAddress { + val url = URL("https://$address") + + return InetSocketAddress(InetAddress.getByName(url.host), url.port) + } + + fun startTun( + fd: Int, + protect: (Int) -> Boolean, + resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String + ) { + startTun(fd, object : TunInterface { + override fun protect(fd: Int) { + protect(fd) + } + + override fun resolverProcess( + protocol: Int, + source: String, + target: String, + uid: Int + ): String { + return resolverProcess( + protocol, + parseInetSocketAddress(source), + parseInetSocketAddress(target), + uid, + ) + } + }); + } + + external fun stopTun() + + init { + System.loadLibrary("core") + } +} \ No newline at end of file diff --git a/android/core/src/main/java/com/follow/clash/core/TunInterface.kt b/android/core/src/main/java/com/follow/clash/core/TunInterface.kt new file mode 100644 index 0000000..ed46d6c --- /dev/null +++ b/android/core/src/main/java/com/follow/clash/core/TunInterface.kt @@ -0,0 +1,9 @@ +package com.follow.clash.core + +import androidx.annotation.Keep + +@Keep +interface TunInterface { + fun protect(fd: Int) + fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index b0ffbea..074e057 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -24,3 +24,4 @@ plugins { } include ":app" +include ':core' diff --git a/core/action.go b/core/action.go index aff017d..d170124 100644 --- a/core/action.go +++ b/core/action.go @@ -35,8 +35,8 @@ func (action Action) getResult(data interface{}) []byte { func handleAction(action *Action, result func(data interface{})) { switch action.Method { case initClashMethod: - data := action.Data.(string) - result(handleInitClash(data)) + paramsString := action.Data.(string) + result(handleInitClash(paramsString)) return case getIsInitMethod: result(handleGetIsInit()) diff --git a/core/android_bride.go b/core/android_bride.go new file mode 100644 index 0000000..640a397 --- /dev/null +++ b/core/android_bride.go @@ -0,0 +1,77 @@ +//go:build android && cgo + +package main + +/* +#include + +typedef void (*release_object_func)(void *obj); + +typedef void (*protect_func)(void *tun_interface, int fd); + +typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid); + +static void protect(protect_func fn, void *tun_interface, int fd) { + if (fn) { + fn(tun_interface, fd); + } +} + +static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) { + if (fn) { + return fn(tun_interface, protocol, source, target, uid); + } + return ""; +} + +static void release_object(release_object_func fn, void *obj) { + if (fn) { + return fn(obj); + } +} +*/ +import "C" +import ( + "unsafe" +) + +var ( + globalCallbacks struct { + releaseObjectFunc C.release_object_func + protectFunc C.protect_func + resolveProcessFunc C.resolve_process_func + } +) + +func protect(callback unsafe.Pointer, fd int) { + if globalCallbacks.protectFunc != nil { + C.protect(globalCallbacks.protectFunc, callback, C.int(fd)) + } +} + +func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string { + if globalCallbacks.resolveProcessFunc == nil { + return "" + } + s := C.CString(source) + defer C.free(unsafe.Pointer(s)) + t := C.CString(target) + defer C.free(unsafe.Pointer(t)) + res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid)) + defer C.free(unsafe.Pointer(res)) + return C.GoString(res) +} + +func releaseObject(callback unsafe.Pointer) { + if globalCallbacks.releaseObjectFunc == nil { + return + } + C.release_object(globalCallbacks.releaseObjectFunc, callback) +} + +//export registerCallbacks +func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) { + globalCallbacks.protectFunc = markSocketFunc + globalCallbacks.resolveProcessFunc = resolveProcessFunc + globalCallbacks.releaseObjectFunc = releaseObjectFunc +} diff --git a/core/common.go b/core/common.go index e34eb7b..2076bd2 100644 --- a/core/common.go +++ b/core/common.go @@ -41,6 +41,7 @@ func splitByMultipleSeparators(s string) interface{} { } var ( + version = 0 isRunning = false runLock sync.Mutex ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"} @@ -274,7 +275,6 @@ 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 a4cb3f7..1da3e33 100644 --- a/core/constant.go +++ b/core/constant.go @@ -7,6 +7,11 @@ import ( "time" ) +type InitParams struct { + HomeDir string `json:"home-dir"` + Version int `json:"version"` +} + type ConfigExtendedParams struct { IsPatch bool `json:"is-patch"` IsCompatible bool `json:"is-compatible"` @@ -71,11 +76,7 @@ const ( stopLogMethod Method = "stopLog" startListenerMethod Method = "startListener" stopListenerMethod Method = "stopListener" - startTunMethod Method = "startTun" - stopTunMethod Method = "stopTun" updateDnsMethod Method = "updateDns" - setProcessMapMethod Method = "setProcessMap" - setFdMapMethod Method = "setFdMap" setStateMethod Method = "setState" getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions" getRunTimeMethod Method = "getRunTime" @@ -109,20 +110,3 @@ func (message *Message) Json() (string, error) { data, err := json.Marshal(message) return string(data), err } - -type InvokeMessage struct { - Type InvokeType `json:"type"` - Data interface{} `json:"data"` -} - -type InvokeType string - -const ( - ProtectInvoke InvokeType = "protect" - ProcessInvoke InvokeType = "process" -) - -func (message *InvokeMessage) Json() string { - data, _ := json.Marshal(message) - return string(data) -} diff --git a/core/hub.go b/core/hub.go index 4ee8452..e92c374 100644 --- a/core/hub.go +++ b/core/hub.go @@ -34,9 +34,15 @@ var ( currentConfig *config.Config ) -func handleInitClash(homeDirStr string) bool { +func handleInitClash(paramsString string) bool { + var params = InitParams{} + err := json.Unmarshal([]byte(paramsString), ¶ms) + if err != nil { + return false + } + version = params.Version if !isInit { - constant.SetHomeDir(homeDirStr) + constant.SetHomeDir(params.HomeDir) isInit = true } return isInit diff --git a/core/lib_android.go b/core/lib_android.go index 880607e..97587de 100644 --- a/core/lib_android.go +++ b/core/lib_android.go @@ -4,6 +4,7 @@ package main import "C" import ( + "context" bridge "core/dart-bridge" "core/platform" "core/state" @@ -11,121 +12,116 @@ import ( "encoding/json" "errors" "fmt" - "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/log" + "golang.org/x/sync/semaphore" + "net" "strconv" "strings" "sync" "syscall" "time" + "unsafe" ) -type Fd struct { - Id string `json:"id"` - Value int64 `json:"value"` +type TunHandler struct { + listener *sing_tun.Listener + callback unsafe.Pointer + + limit *semaphore.Weighted } -type Process struct { - Id string `json:"id"` - Metadata *constant.Metadata `json:"metadata"` -} - -type ProcessMapItem struct { - Id string `json:"id"` - Value string `json:"value"` -} - -type InvokeManager struct { - invokeMap sync.Map - chanMap map[string]chan struct{} - chanLock sync.Mutex -} - -func NewInvokeManager() *InvokeManager { - return &InvokeManager{ - chanMap: make(map[string]chan struct{}), +func (t *TunHandler) close() { + _ = t.limit.Acquire(context.TODO(), 4) + defer t.limit.Release(4) + removeTunHook() + if t.listener != nil { + _ = t.listener.Close() } + + if t.callback != nil { + releaseObject(t.callback) + } + t.callback = nil + t.listener = nil } -func (m *InvokeManager) completer(id string, value string) { - m.invokeMap.Store(id, value) - m.chanLock.Lock() - if ch, ok := m.chanMap[id]; ok { - close(ch) - delete(m.chanMap, id) +func (t *TunHandler) handleProtect(fd int) { + _ = t.limit.Acquire(context.Background(), 1) + defer t.limit.Release(1) + + if t.listener == nil { + return } - m.chanLock.Unlock() + + protect(t.callback, fd) } -func (m *InvokeManager) await(id string) string { - m.chanLock.Lock() - if _, ok := m.chanMap[id]; !ok { - m.chanMap[id] = make(chan struct{}) - } - ch := m.chanMap[id] - m.chanLock.Unlock() +func (t *TunHandler) handleResolveProcess(source, target net.Addr) string { + _ = t.limit.Acquire(context.Background(), 1) + defer t.limit.Release(1) - timeout := time.After(500 * time.Millisecond) - select { - case <-ch: - res, ok := m.invokeMap.Load(id) - m.invokeMap.Delete(id) - if ok { - return res.(string) - } else { - return "" - } - case <-timeout: - m.completer(id, "") + if t.listener == nil { return "" } + var protocol int + uid := -1 + switch source.Network() { + case "udp", "udp4", "udp6": + protocol = syscall.IPPROTO_UDP + case "tcp", "tcp4", "tcp6": + protocol = syscall.IPPROTO_TCP + } + if version < 29 { + uid = platform.QuerySocketUidFromProcFs(source, target) + } + return resolveProcess(t.callback, protocol, source.String(), target.String(), uid) } var ( - invokePort int64 = -1 - tunListener *sing_tun.Listener - fdInvokeMap = NewInvokeManager() - processInvokeMap = NewInvokeManager() - tunLock sync.Mutex - runTime *time.Time - errBlocked = errors.New("blocked") + tunLock sync.Mutex + runTime *time.Time + errBlocked = errors.New("blocked") + tunHandler *TunHandler ) -func handleStartTun(fd int) string { - handleStopTun() - tunLock.Lock() - defer tunLock.Unlock() - if fd == 0 { - now := time.Now() - runTime = &now - } else { - initSocketHook() - tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack) - if tunListener != nil { - log.Infoln("TUN address: %v", tunListener.Address()) - } - now := time.Now() - runTime = &now - } - return handleGetRunTime() -} - func handleStopTun() { tunLock.Lock() defer tunLock.Unlock() - removeSocketHook() runTime = nil - if tunListener != nil { - log.Infoln("TUN close") - _ = tunListener.Close() + if tunHandler != nil { + tunHandler.close() } } +func handleStartTun(fd int, callback unsafe.Pointer) bool { + handleStopTun() + now := time.Now() + runTime = &now + if fd != 0 { + tunLock.Lock() + defer tunLock.Unlock() + tunHandler = &TunHandler{ + callback: callback, + limit: semaphore.NewWeighted(4), + } + initTunHook() + tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack) + if tunListener != nil { + log.Infoln("TUN address: %v", tunListener.Address()) + } else { + removeTunHook() + return false + } + tunHandler.listener = tunListener + } + return true +} + func handleGetRunTime() string { if runTime == nil { return "" @@ -133,83 +129,29 @@ func handleGetRunTime() string { return strconv.FormatInt(runTime.UnixMilli(), 10) } -func handleSetProcessMap(params string) { - var processMapItem = &ProcessMapItem{} - err := json.Unmarshal([]byte(params), processMapItem) - if err == nil { - processInvokeMap.completer(processMapItem.Id, processMapItem.Value) - } -} - -//export attachInvokePort -func attachInvokePort(mPort C.longlong) { - invokePort = int64(mPort) -} - -func sendInvokeMessage(message InvokeMessage) { - if invokePort == -1 { - return - } - bridge.SendToPort(invokePort, message.Json()) -} - -func handleMarkSocket(fd Fd) { - sendInvokeMessage(InvokeMessage{ - Type: ProtectInvoke, - Data: fd, - }) -} - -func handleParseProcess(process Process) { - sendInvokeMessage(InvokeMessage{ - Type: ProcessInvoke, - Data: process, - }) -} - -func handleSetFdMap(id string) { - go func() { - fdInvokeMap.completer(id, "") - }() -} - -func initSocketHook() { +func initTunHook() { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { if platform.ShouldBlockConnection() { return errBlocked } return conn.Control(func(fd uintptr) { - fdInt := int64(fd) - id := utils.NewUUIDV1().String() - - handleMarkSocket(Fd{ - Id: id, - Value: fdInt, - }) - - fdInvokeMap.await(id) + tunHandler.handleProtect(int(fd)) }) } -} - -func removeSocketHook() { - dialer.DefaultSocketHook = nil -} - -func init() { process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) { - if metadata == nil { + src, dst := metadata.RawSrcAddr, metadata.RawDstAddr + if src == nil || dst == nil { return "", process.ErrInvalidNetwork } - id := utils.NewUUIDV1().String() - handleParseProcess(Process{ - Id: id, - Metadata: metadata, - }) - return processInvokeMap.await(id), nil + return tunHandler.handleResolveProcess(src, dst), nil } } +func removeTunHook() { + dialer.DefaultSocketHook = nil + process.DefaultPackageNameResolver = nil +} + func handleGetAndroidVpnOptions() string { tunLock.Lock() defer tunLock.Unlock() @@ -250,16 +192,6 @@ func handleGetCurrentProfileName() string { func nextHandle(action *Action, result func(data interface{})) bool { switch action.Method { - case startTunMethod: - data := action.Data.(string) - var fd int - _ = json.Unmarshal([]byte(data), &fd) - result(handleStartTun(fd)) - return true - case stopTunMethod: - handleStopTun() - result(true) - return true case getAndroidVpnOptionsMethod: result(handleGetAndroidVpnOptions()) return true @@ -268,16 +200,6 @@ func nextHandle(action *Action, result func(data interface{})) bool { handleUpdateDns(data) result(true) return true - case setFdMapMethod: - fdId := action.Data.(string) - handleSetFdMap(fdId) - result(true) - return true - case setProcessMapMethod: - data := action.Data.(string) - handleSetProcessMap(data) - result(true) - return true case getRunTimeMethod: result(handleGetRunTime()) return true @@ -289,13 +211,13 @@ func nextHandle(action *Action, result func(data interface{})) bool { } //export quickStart -func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) { +func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) { i := int64(port) - dir := C.GoString(dirChar) + paramsString := C.GoString(initParamsChar) bytes := []byte(C.GoString(paramsChar)) stateParams := C.GoString(stateParamsChar) go func() { - res := handleInitClash(dir) + res := handleInitClash(paramsString) if res == false { bridge.SendToPort(i, "init error") } @@ -305,9 +227,8 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po } //export startTUN -func startTUN(fd C.int) *C.char { - f := int(fd) - return C.CString(handleStartTun(f)) +func startTUN(fd C.int, callback unsafe.Pointer) bool { + return handleStartTun(int(fd), callback) } //export getRunTime @@ -320,12 +241,6 @@ func stopTun() { handleStopTun() } -//export setFdMap -func setFdMap(fdIdChar *C.char) { - fdId := C.GoString(fdIdChar) - handleSetFdMap(fdId) -} - //export getCurrentProfileName func getCurrentProfileName() *C.char { return C.CString(handleGetCurrentProfileName()) @@ -347,12 +262,3 @@ func updateDns(s *C.char) { dnsList := C.GoString(s) handleUpdateDns(dnsList) } - -//export setProcessMap -func setProcessMap(s *C.char) { - if s == nil { - return - } - paramsString := C.GoString(s) - handleSetProcessMap(paramsString) -} diff --git a/core/platform/procfs.go b/core/platform/procfs.go new file mode 100644 index 0000000..94989c0 --- /dev/null +++ b/core/platform/procfs.go @@ -0,0 +1,176 @@ +//go:build linux +// +build linux + +package platform + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "fmt" + "net" + "os" + "strconv" + "strings" + "unsafe" +) + +var netIndexOfLocal = -1 +var netIndexOfUid = -1 + +var nativeEndian binary.ByteOrder + +func QuerySocketUidFromProcFs(source, _ net.Addr) int { + if netIndexOfLocal < 0 || netIndexOfUid < 0 { + return -1 + } + + network := source.Network() + + if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") { + network = network[:len(network)-1] + } + + path := "/proc/net/" + network + + var sIP net.IP + var sPort int + + switch s := source.(type) { + case *net.TCPAddr: + sIP = s.IP + sPort = s.Port + case *net.UDPAddr: + sIP = s.IP + sPort = s.Port + default: + return -1 + } + + sIP = sIP.To16() + if sIP == nil { + return -1 + } + + uid := doQuery(path+"6", sIP, sPort) + if uid == -1 { + sIP = sIP.To4() + if sIP == nil { + return -1 + } + uid = doQuery(path, sIP, sPort) + } + + return uid +} + +func doQuery(path string, sIP net.IP, sPort int) int { + file, err := os.Open(path) + if err != nil { + return -1 + } + + defer func(file *os.File) { + _ = file.Close() + }(file) + + reader := bufio.NewReader(file) + + var bytes [2]byte + + binary.BigEndian.PutUint16(bytes[:], uint16(sPort)) + + local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:])) + + for { + row, _, err := reader.ReadLine() + if err != nil { + return -1 + } + + fields := strings.Fields(string(row)) + + if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid { + continue + } + + if strings.EqualFold(local, fields[netIndexOfLocal]) { + uid, err := strconv.Atoi(fields[netIndexOfUid]) + if err != nil { + return -1 + } + + return uid + } + } +} + +func nativeEndianIP(ip net.IP) []byte { + result := make([]byte, len(ip)) + + for i := 0; i < len(ip); i += 4 { + value := binary.BigEndian.Uint32(ip[i:]) + + nativeEndian.PutUint32(result[i:], value) + } + + return result +} + +func init() { + file, err := os.Open("/proc/net/tcp") + if err != nil { + return + } + + defer func(file *os.File) { + _ = file.Close() + }(file) + + reader := bufio.NewReader(file) + + header, _, err := reader.ReadLine() + if err != nil { + return + } + + columns := strings.Fields(string(header)) + + var txQueue, rxQueue, tr, tmWhen bool + + for idx, col := range columns { + offset := 0 + + if txQueue && rxQueue { + offset-- + } + + if tr && tmWhen { + offset-- + } + + switch col { + case "tx_queue": + txQueue = true + case "rx_queue": + rxQueue = true + case "tr": + tr = true + case "tm->when": + tmWhen = true + case "local_address": + netIndexOfLocal = idx + offset + case "uid": + netIndexOfUid = idx + offset + } + } +} + +func init() { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + nativeEndian = binary.BigEndian + } else { + nativeEndian = binary.LittleEndian + } +} diff --git a/lib/application.dart b/lib/application.dart index 416531a..1a3027d 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -7,7 +7,7 @@ import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/plugins/app.dart'; -import 'package:fl_clash/providers/config.dart'; +import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -43,16 +43,8 @@ class ApplicationState extends ConsumerState { ColorScheme _getAppColorScheme({ required Brightness brightness, int? primaryColor, - required ColorSchemes systemColorSchemes, }) { - if (primaryColor != null) { - return ColorScheme.fromSeed( - seedColor: Color(primaryColor), - brightness: brightness, - ); - } else { - return systemColorSchemes.getColorSchemeForBrightness(brightness); - } + return ref.read(genColorSchemeProvider(brightness)); } @override @@ -183,7 +175,7 @@ class ApplicationState extends ConsumerState { }, scrollBehavior: BaseScrollBehavior(), title: appName, - locale: other.getLocaleForString(locale), + locale: utils.getLocaleForString(locale), supportedLocales: AppLocalizations.delegate.supportedLocales, themeMode: themeProps.themeMode, theme: ThemeData( @@ -191,7 +183,6 @@ class ApplicationState extends ConsumerState { pageTransitionsTheme: _pageTransitionsTheme, colorScheme: _getAppColorScheme( brightness: Brightness.light, - systemColorSchemes: systemColorSchemes, primaryColor: themeProps.primaryColor, ), ), @@ -200,7 +191,6 @@ class ApplicationState extends ConsumerState { pageTransitionsTheme: _pageTransitionsTheme, colorScheme: _getAppColorScheme( brightness: Brightness.dark, - systemColorSchemes: systemColorSchemes, primaryColor: themeProps.primaryColor, ).toPureBlack(themeProps.pureBlack), ), diff --git a/lib/clash/core.dart b/lib/clash/core.dart index b5ae232..2a880f6 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -8,6 +8,7 @@ import 'package:fl_clash/clash/interface.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart'; @@ -66,7 +67,12 @@ class ClashCore { Future init() async { await initGeo(); final homeDirPath = await appPath.homeDirPath; - return await clashInterface.init(homeDirPath); + return await clashInterface.init( + InitParams( + homeDir: homeDirPath, + version: globalState.appState.version, + ), + ); } Future setState(CoreState state) async { diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index cd85477..d20959d 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -2348,6 +2348,97 @@ class ClashFFI { set suboptarg(ffi.Pointer value) => _suboptarg.value = value; + void protect( + protect_func fn, + ffi.Pointer tun_interface, + int fd, + ) { + return _protect( + fn, + tun_interface, + fd, + ); + } + + late final _protectPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + protect_func, ffi.Pointer, ffi.Int)>>('protect'); + late final _protect = _protectPtr + .asFunction, int)>(); + + ffi.Pointer resolve_process( + resolve_process_func fn, + ffi.Pointer tun_interface, + int protocol, + ffi.Pointer source, + ffi.Pointer target, + int uid, + ) { + return _resolve_process( + fn, + tun_interface, + protocol, + source, + target, + uid, + ); + } + + late final _resolve_processPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + resolve_process_func, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Int)>>('resolve_process'); + late final _resolve_process = _resolve_processPtr.asFunction< + ffi.Pointer Function( + resolve_process_func, + ffi.Pointer, + int, + ffi.Pointer, + ffi.Pointer, + int)>(); + + void release_object( + release_object_func fn, + ffi.Pointer obj, + ) { + return _release_object( + fn, + obj, + ); + } + + late final _release_objectPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + release_object_func, ffi.Pointer)>>('release_object'); + late final _release_object = _release_objectPtr + .asFunction)>(); + + void registerCallbacks( + protect_func markSocketFunc, + resolve_process_func resolveProcessFunc, + release_object_func releaseObjectFunc, + ) { + return _registerCallbacks( + markSocketFunc, + resolveProcessFunc, + releaseObjectFunc, + ); + } + + late final _registerCallbacksPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(protect_func, resolve_process_func, + release_object_func)>>('registerCallbacks'); + late final _registerCallbacks = _registerCallbacksPtr.asFunction< + void Function(protect_func, resolve_process_func, release_object_func)>(); + void initNativeApiBridge( ffi.Pointer api, ) { @@ -2443,28 +2534,14 @@ class ClashFFI { _lookup>('stopListener'); late final _stopListener = _stopListenerPtr.asFunction(); - void attachInvokePort( - int mPort, - ) { - return _attachInvokePort( - mPort, - ); - } - - late final _attachInvokePortPtr = - _lookup>( - 'attachInvokePort'); - late final _attachInvokePort = - _attachInvokePortPtr.asFunction(); - void quickStart( - ffi.Pointer dirChar, + ffi.Pointer initParamsChar, ffi.Pointer paramsChar, ffi.Pointer stateParamsChar, int port, ) { return _quickStart( - dirChar, + initParamsChar, paramsChar, stateParamsChar, port, @@ -2479,19 +2556,21 @@ class ClashFFI { void Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, int)>(); - ffi.Pointer startTUN( + int startTUN( int fd, + ffi.Pointer callback, ) { return _startTUN( fd, + callback, ); } - late final _startTUNPtr = - _lookup Function(ffi.Int)>>( - 'startTUN'); + late final _startTUNPtr = _lookup< + ffi.NativeFunction)>>( + 'startTUN'); late final _startTUN = - _startTUNPtr.asFunction Function(int)>(); + _startTUNPtr.asFunction)>(); ffi.Pointer getRunTime() { return _getRunTime(); @@ -2511,20 +2590,6 @@ class ClashFFI { _lookup>('stopTun'); late final _stopTun = _stopTunPtr.asFunction(); - void setFdMap( - ffi.Pointer fdIdChar, - ) { - return _setFdMap( - fdIdChar, - ); - } - - late final _setFdMapPtr = - _lookup)>>( - 'setFdMap'); - late final _setFdMap = - _setFdMapPtr.asFunction)>(); - ffi.Pointer getCurrentProfileName() { return _getCurrentProfileName(); } @@ -2572,20 +2637,6 @@ class ClashFFI { 'updateDns'); late final _updateDns = _updateDnsPtr.asFunction)>(); - - void setProcessMap( - ffi.Pointer s, - ) { - return _setProcessMap( - s, - ); - } - - late final _setProcessMapPtr = - _lookup)>>( - 'setProcessMap'); - late final _setProcessMap = - _setProcessMapPtr.asFunction)>(); } final class __mbstate_t extends ffi.Union { @@ -3738,6 +3789,31 @@ typedef mode_t = __darwin_mode_t; typedef __darwin_mode_t = __uint16_t; typedef __uint16_t = ffi.UnsignedShort; typedef Dart__uint16_t = int; +typedef protect_func = ffi.Pointer>; +typedef protect_funcFunction = ffi.Void Function( + ffi.Pointer tun_interface, ffi.Int fd); +typedef Dartprotect_funcFunction = void Function( + ffi.Pointer tun_interface, int fd); +typedef resolve_process_func + = ffi.Pointer>; +typedef resolve_process_funcFunction = ffi.Pointer Function( + ffi.Pointer tun_interface, + ffi.Int protocol, + ffi.Pointer source, + ffi.Pointer target, + ffi.Int uid); +typedef Dartresolve_process_funcFunction = ffi.Pointer Function( + ffi.Pointer tun_interface, + int protocol, + ffi.Pointer source, + ffi.Pointer target, + int uid); +typedef release_object_func + = ffi.Pointer>; +typedef release_object_funcFunction = ffi.Void Function( + ffi.Pointer obj); +typedef Dartrelease_object_funcFunction = void Function( + ffi.Pointer obj); final class GoInterface extends ffi.Struct { external ffi.Pointer t; @@ -3758,6 +3834,8 @@ final class GoSlice extends ffi.Struct { typedef GoInt = GoInt64; typedef GoInt64 = ffi.LongLong; typedef DartGoInt64 = int; +typedef GoUint8 = ffi.UnsignedChar; +typedef DartGoUint8 = int; const int __has_safe_buffers = 1; @@ -3973,6 +4051,8 @@ const int __MAC_15_0 = 150000; const int __MAC_15_1 = 150100; +const int __MAC_15_2 = 150200; + const int __IPHONE_2_0 = 20000; const int __IPHONE_2_1 = 20100; @@ -4135,6 +4215,8 @@ const int __IPHONE_18_0 = 180000; const int __IPHONE_18_1 = 180100; +const int __IPHONE_18_2 = 180200; + const int __WATCHOS_1_0 = 10000; const int __WATCHOS_2_0 = 20000; @@ -4233,6 +4315,8 @@ const int __WATCHOS_11_0 = 110000; const int __WATCHOS_11_1 = 110100; +const int __WATCHOS_11_2 = 110200; + const int __TVOS_9_0 = 90000; const int __TVOS_9_1 = 90100; @@ -4333,6 +4417,8 @@ const int __TVOS_18_0 = 180000; const int __TVOS_18_1 = 180100; +const int __TVOS_18_2 = 180200; + const int __BRIDGEOS_2_0 = 20000; const int __BRIDGEOS_3_0 = 30000; @@ -4389,6 +4475,8 @@ const int __BRIDGEOS_9_0 = 90000; const int __BRIDGEOS_9_1 = 90100; +const int __BRIDGEOS_9_2 = 90200; + const int __DRIVERKIT_19_0 = 190000; const int __DRIVERKIT_20_0 = 200000; @@ -4419,6 +4507,8 @@ const int __DRIVERKIT_24_0 = 240000; const int __DRIVERKIT_24_1 = 240100; +const int __DRIVERKIT_24_2 = 240200; + const int __VISIONOS_1_0 = 10000; const int __VISIONOS_1_1 = 10100; @@ -4429,6 +4519,8 @@ const int __VISIONOS_2_0 = 20000; const int __VISIONOS_2_1 = 20100; +const int __VISIONOS_2_2 = 20200; + const int MAC_OS_X_VERSION_10_0 = 1000; const int MAC_OS_X_VERSION_10_1 = 1010; @@ -4555,9 +4647,11 @@ const int MAC_OS_VERSION_15_0 = 150000; const int MAC_OS_VERSION_15_1 = 150100; +const int MAC_OS_VERSION_15_2 = 150200; + const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000; -const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100; +const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150200; const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1; diff --git a/lib/clash/interface.dart b/lib/clash/interface.dart index 1d62631..b0f11ea 100644 --- a/lib/clash/interface.dart +++ b/lib/clash/interface.dart @@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; mixin ClashInterface { - Future init(String homeDir); + Future init(InitParams params); Future preload(); @@ -74,12 +74,10 @@ mixin AndroidClashInterface { Future setProcessMap(ProcessMapItem item); - Future stopTun(); + // Future stopTun(); Future updateDns(String value); - Future startTun(int fd); - Future getAndroidVpnOptions(); Future getCurrentProfileName(); @@ -153,7 +151,7 @@ abstract class ClashHandlerInterface with ClashInterface { Duration? timeout, FutureOr Function()? onTimeout, }) async { - final id = "${method.name}#${other.id}"; + final id = "${method.name}#${utils.id}"; callbackCompleterMap[id] = Completer(); @@ -191,10 +189,10 @@ abstract class ClashHandlerInterface with ClashInterface { } @override - Future init(String homeDir) { + Future init(InitParams params) { return invoke( method: ActionMethod.initClash, - data: homeDir, + data: json.encode(params), ); } diff --git a/lib/clash/lib.dart b/lib/clash/lib.dart index a8182f4..9de8a2a 100644 --- a/lib/clash/lib.dart +++ b/lib/clash/lib.dart @@ -122,25 +122,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface { ); } - @override - Future startTun(int fd) async { - final res = await invoke( - method: ActionMethod.startTun, - data: json.encode(fd), - ); - - if (res.isEmpty) { - return null; - } - return DateTime.fromMillisecondsSinceEpoch(int.parse(res)); - } - - @override - Future stopTun() { - return invoke( - method: ActionMethod.stopTun, - ); - } + // @override + // Future stopTun() { + // return invoke( + // method: ActionMethod.stopTun, + // ); + // } @override Future getAndroidVpnOptions() async { @@ -224,37 +211,12 @@ class ClashLibHandler { ); } - attachInvokePort(int invokePort) { - clashFFI.attachInvokePort( - invokePort, - ); - } - - DateTime? startTun(int fd) { - final runTimeRaw = clashFFI.startTUN(fd); - final runTimeString = runTimeRaw.cast().toDartString(); - clashFFI.freeCString(runTimeRaw); - if (runTimeString.isEmpty) return null; - return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); - } - - stopTun() { - clashFFI.stopTun(); - } - updateDns(String dns) { final dnsChar = dns.toNativeUtf8().cast(); clashFFI.updateDns(dnsChar); malloc.free(dnsChar); } - setProcessMap(ProcessMapItem processMapItem) { - final processMapItemChar = - json.encode(processMapItem).toNativeUtf8().cast(); - clashFFI.setProcessMap(processMapItemChar); - malloc.free(processMapItemChar); - } - setState(CoreState state) { final stateChar = json.encode(state).toNativeUtf8().cast(); clashFFI.setState(stateChar); @@ -305,17 +267,11 @@ class ClashLibHandler { return true; } - setFdMap(String id) { - final idChar = id.toNativeUtf8().cast(); - clashFFI.setFdMap(idChar); - malloc.free(idChar); - } - Future quickStart( - String homeDir, - UpdateConfigParams updateConfigParams, - CoreState state, - ) { + InitParams initParams, + UpdateConfigParams updateConfigParams, + CoreState state, + ) { final completer = Completer(); final receiver = ReceivePort(); receiver.listen((message) { @@ -325,17 +281,18 @@ class ClashLibHandler { } }); final params = json.encode(updateConfigParams); + final initValue = json.encode(initParams); final stateParams = json.encode(state); - final homeChar = homeDir.toNativeUtf8().cast(); + final initParamsChar = initValue.toNativeUtf8().cast(); final paramsChar = params.toNativeUtf8().cast(); final stateParamsChar = stateParams.toNativeUtf8().cast(); clashFFI.quickStart( - homeChar, + initParamsChar, paramsChar, stateParamsChar, receiver.sendPort.nativePort, ); - malloc.free(homeChar); + malloc.free(initParamsChar); malloc.free(paramsChar); malloc.free(stateParamsChar); return completer.future; diff --git a/lib/common/color.dart b/lib/common/color.dart index f81b55e..2a71752 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; extension ColorExtension on Color { @@ -37,11 +39,54 @@ extension ColorExtension on Color { return withAlpha(0); } - Color darken([double amount = .1]) { - assert(amount >= 0 && amount <= 1); - final hsl = HSLColor.fromColor(this); - final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); - return hslDark.toColor(); + int get value32bit { + return _floatToInt8(a) << 24 | + _floatToInt8(r) << 16 | + _floatToInt8(g) << 8 | + _floatToInt8(b) << 0; + } + + int get alpha8bit => (0xff000000 & value32bit) >> 24; + + int get red8bit => (0x00ff0000 & value32bit) >> 16; + + int get green8bit => (0x0000ff00 & value32bit) >> 8; + + int get blue8bit => (0x000000ff & value32bit) >> 0; + + int _floatToInt8(double x) { + return (x * 255.0).round() & 0xff; + } + + Color lighten([double amount = 10]) { + if (amount <= 0) return this; + if (amount > 100) return Colors.white; + final HSLColor hsl = this == const Color(0xFF000000) + ? HSLColor.fromColor(this).withSaturation(0) + : HSLColor.fromColor(this); + return hsl + .withLightness(min(1, max(0, hsl.lightness + amount / 100))) + .toColor(); + } + + String get hex { + final value = toARGB32(); + final red = (value >> 16) & 0xFF; + final green = (value >> 8) & 0xFF; + final blue = value & 0xFF; + return '#${red.toRadixString(16).padLeft(2, '0')}' + '${green.toRadixString(16).padLeft(2, '0')}' + '${blue.toRadixString(16).padLeft(2, '0')}' + .toUpperCase(); + } + + Color darken([final int amount = 10]) { + if (amount <= 0) return this; + if (amount > 100) return Colors.black; + final HSLColor hsl = HSLColor.fromColor(this); + return hsl + .withLightness(min(1, max(0, hsl.lightness - amount / 100))) + .toColor(); } Color blendDarken( @@ -74,7 +119,7 @@ extension ColorSchemeExtension on ColorScheme { ? copyWith( surface: Colors.black, surfaceContainer: surfaceContainer.darken( - 0.05, + 5, ), ) : this; diff --git a/lib/common/common.dart b/lib/common/common.dart index 4245f4c..1b95ccf 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -19,7 +19,7 @@ export 'navigation.dart'; export 'navigator.dart'; export 'network.dart'; export 'num.dart'; -export 'other.dart'; +export 'utils.dart'; export 'package.dart'; export 'path.dart'; export 'picker.dart'; diff --git a/lib/common/constant.dart b/lib/common/constant.dart index a71dfc3..907cd7d 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -78,7 +78,7 @@ const viewModeColumnsMap = { ViewMode.desktop: [4, 3], }; -const defaultPrimaryColor = Colors.brown; +const defaultPrimaryColor = 0xFF795548; double getWidgetHeight(num lines) { return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0); @@ -87,3 +87,13 @@ double getWidgetHeight(num lines) { final mainIsolate = "FlClashMainIsolate"; final serviceIsolate = "FlClashServiceIsolate"; + +const defaultPrimaryColors = [ + defaultPrimaryColor, + 0xFF03A9F4, + 0xFFFFFF00, + 0XFFBBC9CC, + 0XFFABD397, + 0XFFD8C0C3, + 0XFF665390, +]; diff --git a/lib/common/launch.dart b/lib/common/launch.dart index d1a52dc..5a6c541 100644 --- a/lib/common/launch.dart +++ b/lib/common/launch.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'constant.dart'; @@ -34,6 +35,9 @@ class AutoLaunch { } updateStatus(bool isAutoLaunch) async { + if(kDebugMode){ + return; + } if (await isEnable == isAutoLaunch) return; if (isAutoLaunch == true) { enable(); diff --git a/lib/common/render.dart b/lib/common/render.dart index eb52f96..8d9cd42 100644 --- a/lib/common/render.dart +++ b/lib/common/render.dart @@ -54,4 +54,4 @@ class Render { } } -final render = system.isDesktop ? Render() : null; +final Render? render = system.isDesktop ? Render() : null; diff --git a/lib/common/request.dart b/lib/common/request.dart index eee6e5a..c0fad05 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -68,7 +68,7 @@ class Request { final remoteVersion = data['tag_name']; final version = globalState.packageInfo.version; final hasUpdate = - other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; + utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; if (!hasUpdate) return null; return data; } diff --git a/lib/common/tray.dart b/lib/common/tray.dart index 69ace6d..f2e744c 100644 --- a/lib/common/tray.dart +++ b/lib/common/tray.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fl_clash/common/utils.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -10,7 +11,6 @@ import 'package:tray_manager/tray_manager.dart'; import 'app_localizations.dart'; import 'constant.dart'; -import 'other.dart'; import 'window.dart'; class Tray { @@ -25,7 +25,7 @@ class Tray { await trayManager.destroy(); } await trayManager.setIcon( - other.getTrayIconPath( + utils.getTrayIconPath( brightness: brightness ?? WidgetsBinding.instance.platformDispatcher.platformBrightness, ), diff --git a/lib/common/other.dart b/lib/common/utils.dart similarity index 82% rename from lib/common/other.dart rename to lib/common/utils.dart index 3bdfaad..49a869f 100644 --- a/lib/common/other.dart +++ b/lib/common/utils.dart @@ -7,7 +7,7 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; import 'package:lpinyin/lpinyin.dart'; -class Other { +class Utils { Color? getDelayColor(int? delay) { if (delay == null) return null; if (delay < 0) return Colors.red; @@ -233,6 +233,63 @@ class Other { return max((viewWidth / 350).floor(), 1); } + final _indexPrimary = [ + 50, + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 850, + 900, + ]; + + _createPrimarySwatch(Color color) { + final Map swatch = {}; + final int a = color.alpha8bit; + final int r = color.red8bit; + final int g = color.green8bit; + final int b = color.blue8bit; + for (final int strength in _indexPrimary) { + final double ds = 0.5 - strength / 1000; + swatch[strength] = Color.fromARGB( + a, + r + ((ds < 0 ? r : (255 - r)) * ds).round(), + g + ((ds < 0 ? g : (255 - g)) * ds).round(), + b + ((ds < 0 ? b : (255 - b)) * ds).round(), + ); + } + swatch[50] = swatch[50]!.lighten(18); + swatch[100] = swatch[100]!.lighten(16); + swatch[200] = swatch[200]!.lighten(14); + swatch[300] = swatch[300]!.lighten(10); + swatch[400] = swatch[400]!.lighten(6); + swatch[700] = swatch[700]!.darken(2); + swatch[800] = swatch[800]!.darken(3); + swatch[900] = swatch[900]!.darken(4); + return MaterialColor(color.value32bit, swatch); + } + + List getMaterialColorShades(Color color) { + final swatch = _createPrimarySwatch(color); + return [ + if (swatch[50] != null) swatch[50]!, + if (swatch[100] != null) swatch[100]!, + if (swatch[200] != null) swatch[200]!, + if (swatch[300] != null) swatch[300]!, + if (swatch[400] != null) swatch[400]!, + if (swatch[500] != null) swatch[500]!, + if (swatch[600] != null) swatch[600]!, + if (swatch[700] != null) swatch[700]!, + if (swatch[800] != null) swatch[800]!, + if (swatch[850] != null) swatch[850]!, + if (swatch[900] != null) swatch[900]!, + ]; + } + String getBackupFileName() { return "${appName}_backup_${DateTime.now().show}.zip"; } @@ -268,4 +325,4 @@ class Other { } } -final other = Other(); +final utils = Utils(); diff --git a/lib/controller.dart b/lib/controller.dart index 65de038..a7b99c6 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -30,8 +30,9 @@ class AppController { AppController(this.context, WidgetRef ref) : _ref = ref; updateClashConfigDebounce() { - debouncer.call(DebounceTag.updateClashConfig, () { - updateClashConfig(true); + debouncer.call(DebounceTag.updateClashConfig, () async { + final isPatch = globalState.appState.needApply ? false : true; + await updateClashConfig(isPatch); }); } @@ -70,7 +71,7 @@ class AppController { restartCore() async { await clashService?.reStart(); - await initCore(); + await _initCore(); if (_ref.read(runTimeProvider.notifier).isStart) { await globalState.handleStart(); @@ -100,7 +101,6 @@ class AppController { _ref.read(trafficsProvider.notifier).clear(); _ref.read(totalTrafficProvider.notifier).value = Traffic(); _ref.read(runTimeProvider.notifier).value = null; - // tray.updateTrayTitle(null); addCheckIpNumDebounce(); } } @@ -153,7 +153,7 @@ class AppController { updateLocalIp() async { _ref.read(localIpProvider.notifier).value = null; await Future.delayed(commonDuration); - _ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress(); + _ref.read(localIpProvider.notifier).value = await utils.getLocalIpAddress(); } Future updateProfile(Profile profile) async { @@ -283,6 +283,9 @@ class AppController { final res = await clashCore.updateConfig( globalState.getUpdateConfigParams(isPatch), ); + if (isPatch == false) { + _ref.read(needApplyProvider.notifier).value = false; + } if (res.isNotEmpty) throw res; lastTunEnable = enableTun; lastProfileModified = await profile?.profileLastModified; @@ -417,13 +420,13 @@ class AppController { Map? data, bool handleError = false, }) async { - if(globalState.isPre){ + if (globalState.isPre) { return; } if (data != null) { final tagName = data['tag_name']; final body = data['body']; - final submits = other.parseReleaseBody(body); + final submits = utils.parseReleaseBody(body); final textTheme = context.textTheme; final res = await globalState.showMessage( title: appLocalizations.discoverNewVersion, @@ -478,7 +481,7 @@ class AppController { await handleExit(); } - Future initCore() async { + Future _initCore() async { final isInit = await clashCore.isInit; if (!isInit) { await clashCore.setState( @@ -492,7 +495,7 @@ class AppController { init() async { await _handlePreference(); await _handlerDisclaimer(); - await initCore(); + await _initCore(); await _initStatus(); updateTray(true); autoLaunch?.updateStatus( @@ -668,9 +671,9 @@ class AppController { List _sortOfName(List proxies) { return List.of(proxies) ..sort( - (a, b) => other.sortByChar( - other.getPinyin(a.name), - other.getPinyin(b.name), + (a, b) => utils.sortByChar( + utils.getPinyin(a.name), + utils.getPinyin(b.name), ), ); } @@ -860,7 +863,7 @@ class AppController { return utf8.encode(logsRawString); }); return await picker.saveFile( - other.logFile, + utils.logFile, Uint8List.fromList(data), ) != null; diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index b730490..2d963c0 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -75,7 +75,7 @@ class BackupAndRecovery extends ConsumerWidget { () async { final backupData = await globalState.appController.backupData(); final value = await picker.saveFile( - other.getBackupFileName(), + utils.getBackupFileName(), Uint8List.fromList(backupData), ); if (value == null) return false; diff --git a/lib/fragments/config/config.dart b/lib/fragments/config/config.dart index 5cbb8bf..d4c8bf8 100644 --- a/lib/fragments/config/config.dart +++ b/lib/fragments/config/config.dart @@ -61,7 +61,6 @@ class _ConfigFragmentState extends State { if (res != true) { return; } - ref.read(patchClashConfigProvider.notifier).updateState( (state) => state.copyWith( dns: defaultDns, diff --git a/lib/fragments/connection/requests.dart b/lib/fragments/connection/requests.dart index 69e04e4..ba99fe2 100644 --- a/lib/fragments/connection/requests.dart +++ b/lib/fragments/connection/requests.dart @@ -11,8 +11,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'item.dart'; -double _preOffset = 0; - class RequestsFragment extends ConsumerStatefulWidget { const RequestsFragment({super.key}); @@ -26,10 +24,8 @@ class _RequestsFragmentState extends ConsumerState final _requestsStateNotifier = ValueNotifier(const ConnectionsState()); List _requests = []; - - final ScrollController _scrollController = ScrollController( - initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, - ); + final _cacheKey = ValueKey("requests_list"); + late ScrollController _scrollController; double _currentMaxWidth = 0; @@ -49,10 +45,13 @@ class _RequestsFragmentState extends ConsumerState @override void initState() { super.initState(); + final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1; + _scrollController = ScrollController( + initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite, + ); _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( connections: globalState.appState.requests.list, ); - ref.listenManual( isCurrentPageProvider( PageLabel.requests, @@ -177,11 +176,10 @@ class _RequestsFragmentState extends ConsumerState .toList(); return Align( alignment: Alignment.topCenter, - child: NotificationListener( - onNotification: (details) { - _preOffset = details.metrics.pixels; - return false; - }, + child: ScrollToEndBox( + controller: _scrollController, + cacheKey: _cacheKey, + dataSource: connections, child: CommonScrollBar( controller: _scrollController, child: CacheItemExtentListView( diff --git a/lib/fragments/dashboard/widgets/start_button.dart b/lib/fragments/dashboard/widgets/start_button.dart index f7a0c0b..350b36c 100644 --- a/lib/fragments/dashboard/widgets/start_button.dart +++ b/lib/fragments/dashboard/widgets/start_button.dart @@ -71,10 +71,10 @@ class _StartButtonState extends State final textWidth = globalState.measure .computeTextSize( Text( - other.getTimeDifference( + utils.getTimeDifference( DateTime.now(), ), - style: Theme.of(context).textTheme.titleMedium?.toSoftBold, + style: context.textTheme.titleMedium?.toSoftBold, ), ) .width + @@ -123,10 +123,12 @@ class _StartButtonState extends State child: Consumer( builder: (_, ref, __) { final runTime = ref.watch(runTimeProvider); - final text = other.getTimeText(runTime); + final text = utils.getTimeText(runTime); return Text( text, - style: Theme.of(context).textTheme.titleMedium?.toSoftBold, + style: Theme.of(context).textTheme.titleMedium?.toSoftBold.copyWith( + color: context.colorScheme.onPrimaryContainer + ), ); }, ), diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index 1333be0..af09186 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -8,8 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/models.dart'; import '../widgets/widgets.dart'; -double _preOffset = 0; - class LogsFragment extends ConsumerStatefulWidget { const LogsFragment({super.key}); @@ -19,9 +17,8 @@ class LogsFragment extends ConsumerStatefulWidget { class _LogsFragmentState extends ConsumerState with PageMixin { final _logsStateNotifier = ValueNotifier(LogsState()); - final _scrollController = ScrollController( - initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, - ); + final _cacheKey = ValueKey("logs_list"); + late ScrollController _scrollController; double _currentMaxWidth = 0; final GlobalKey _key = GlobalKey(); @@ -30,6 +27,10 @@ class _LogsFragmentState extends ConsumerState with PageMixin { @override void initState() { super.initState(); + final preOffset = globalState.cacheScrollPosition[_cacheKey] ?? -1; + _scrollController = ScrollController( + initialScrollOffset: preOffset > 0 ? preOffset : double.maxFinite, + ); _logsStateNotifier.value = _logsStateNotifier.value.copyWith( logs: globalState.appState.logs.list, ); @@ -180,11 +181,10 @@ class _LogsFragmentState extends ConsumerState with PageMixin { ), ) .toList(); - return NotificationListener( - onNotification: (details) { - _preOffset = details.metrics.pixels; - return false; - }, + return ScrollToEndBox( + controller: _scrollController, + cacheKey: _cacheKey, + dataSource: logs, child: CommonScrollBar( controller: _scrollController, child: CacheItemExtentListView( diff --git a/lib/fragments/profiles/override_profile.dart b/lib/fragments/profiles/override_profile.dart index bb3d8fb..f1d0afb 100644 --- a/lib/fragments/profiles/override_profile.dart +++ b/lib/fragments/profiles/override_profile.dart @@ -45,6 +45,7 @@ class _OverrideProfileState extends State { } _handleSave(WidgetRef ref, OverrideData overrideData) { + ref.read(needApplyProvider.notifier).value = true; ref.read(profilesProvider.notifier).updateProfile( widget.profileId, (state) => state.copyWith( diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index 4433335..566e1dc 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -63,7 +63,7 @@ class ProxyCard extends StatelessWidget { delay > 0 ? '$delay ms' : "Timeout", style: context.textTheme.labelSmall?.copyWith( overflow: TextOverflow.ellipsis, - color: other.getDelayColor( + color: utils.getDelayColor( delay, ), ), diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index 1d62d31..cbf7a5f 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -1,9 +1,16 @@ +import 'dart:math'; + +import 'dart:ui' as ui; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/selector.dart'; import 'package:fl_clash/providers/config.dart'; +import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; class ThemeModeItem { final ThemeMode themeMode; @@ -32,39 +39,20 @@ class ThemeFragment extends StatelessWidget { @override Widget build(BuildContext context) { - final previewCard = Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: CommonCard( - onPressed: () {}, - info: Info( - label: appLocalizations.preview, - iconData: Icons.looks, - ), - child: Container( - height: 200, - ), - ), - ); - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - previewCard, - const ThemeColorsBox(), - ], - ), - ); + return SingleChildScrollView(child: ThemeColorsBox()); } } class ItemCard extends StatelessWidget { final Widget child; final Info info; + final List actions; const ItemCard({ super.key, required this.info, required this.child, + this.actions = const [], }); @override @@ -78,6 +66,7 @@ class ItemCard extends StatelessWidget { children: [ InfoHeader( info: info, + actions: actions, ), child, ], @@ -98,7 +87,6 @@ class _ThemeColorsBoxState extends ConsumerState { Widget build(BuildContext context) { return Column( children: [ - // _FontFamilyItem(), _ThemeModeItem(), _PrimaryColorItem(), _PrueBlackItem(), @@ -110,75 +98,6 @@ class _ThemeColorsBoxState extends ConsumerState { } } -// class _FontFamilyItem extends ConsumerWidget { -// const _FontFamilyItem(); -// -// @override -// Widget build(BuildContext context, WidgetRef ref) { -// final fontFamily = -// ref.watch(themeSettingProvider.select((state) => state.fontFamily)); -// List fontFamilyItems = [ -// FontFamilyItem( -// label: appLocalizations.systemFont, -// fontFamily: FontFamily.system, -// ), -// const FontFamilyItem( -// label: "roboto", -// fontFamily: FontFamily.roboto, -// ), -// ]; -// return ItemCard( -// info: Info( -// label: appLocalizations.fontFamily, -// iconData: Icons.text_fields, -// ), -// child: Container( -// margin: const EdgeInsets.only( -// left: 16, -// right: 16, -// ), -// height: 48, -// child: ListView.separated( -// scrollDirection: Axis.horizontal, -// itemBuilder: (_, index) { -// final fontFamilyItem = fontFamilyItems[index]; -// return CommonCard( -// isSelected: fontFamilyItem.fontFamily == fontFamily, -// onPressed: () { -// ref.read(themeSettingProvider.notifier).updateState( -// (state) => state.copyWith( -// fontFamily: fontFamilyItem.fontFamily, -// ), -// ); -// }, -// child: Padding( -// padding: const EdgeInsets.symmetric(horizontal: 16), -// child: Row( -// mainAxisSize: MainAxisSize.min, -// mainAxisAlignment: MainAxisAlignment.start, -// children: [ -// Flexible( -// child: Text( -// fontFamilyItem.label, -// ), -// ), -// ], -// ), -// ), -// ); -// }, -// separatorBuilder: (_, __) { -// return const SizedBox( -// width: 16, -// ); -// }, -// itemCount: fontFamilyItems.length, -// ), -// ), -// ); -// } -// } - class _ThemeModeItem extends ConsumerWidget { const _ThemeModeItem(); @@ -210,7 +129,7 @@ class _ThemeModeItem extends ConsumerWidget { ), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), - height: 64, + height: 56, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: themeModeItems.length, @@ -258,56 +177,256 @@ class _ThemeModeItem extends ConsumerWidget { } } -class _PrimaryColorItem extends ConsumerWidget { +class _PrimaryColorItem extends ConsumerStatefulWidget { const _PrimaryColorItem(); @override - Widget build(BuildContext context, WidgetRef ref) { - final primaryColor = - ref.watch(themeSettingProvider.select((state) => state.primaryColor)); - List primaryColors = [ - null, - defaultPrimaryColor, - Colors.pinkAccent, - Colors.lightBlue, - Colors.greenAccent, - Colors.yellowAccent, - Colors.purple, - ]; + ConsumerState<_PrimaryColorItem> createState() => _PrimaryColorItemState(); +} + +class _PrimaryColorItemState extends ConsumerState<_PrimaryColorItem> { + int? _removablePrimaryColor; + + int _calcColumns(double maxWidth) { + return max((maxWidth / 96).ceil(), 3); + } + + _handleReset() async { + final res = await globalState.showMessage( + message: TextSpan( + text: appLocalizations.resetTip, + ), + ); + if (res != true) { + return; + } + ref.read(themeSettingProvider.notifier).updateState( + (state) { + return state.copyWith( + primaryColors: defaultPrimaryColors, + primaryColor: defaultPrimaryColor, + schemeVariant: DynamicSchemeVariant.tonalSpot, + ); + }, + ); + } + + _handleDel() async { + if (_removablePrimaryColor == null) { + return; + } + final res = await globalState.showMessage( + message: TextSpan(text: appLocalizations.deleteColorTip)); + if (res != true) { + return; + } + ref.read(themeSettingProvider.notifier).updateState( + (state) { + final newPrimaryColors = List.from(state.primaryColors) + ..remove(_removablePrimaryColor); + int? newPrimaryColor = state.primaryColor; + if (state.primaryColor == _removablePrimaryColor) { + if (newPrimaryColors.contains(defaultPrimaryColor)) { + newPrimaryColor = defaultPrimaryColor; + } else { + newPrimaryColor = null; + } + } + return state.copyWith( + primaryColors: newPrimaryColors, + primaryColor: newPrimaryColor, + ); + }, + ); + setState(() { + _removablePrimaryColor = null; + }); + } + + _handleAdd() async { + final res = await globalState.showCommonDialog( + child: _PaletteDialog(), + ); + if (res == null) { + return; + } + final isExists = ref.read( + themeSettingProvider.select((state) => state.primaryColors.contains(res)), + ); + if (isExists && mounted) { + context.showNotifier(appLocalizations.colorExists); + return; + } + ref.read(themeSettingProvider.notifier).updateState( + (state) { + return state.copyWith( + primaryColors: List.from( + state.primaryColors, + )..add(res), + ); + }, + ); + } + + _handleChangeSchemeVariant() async { + final schemeVariant = ref.read( + themeSettingProvider.select( + (state) => state.schemeVariant, + ), + ); + final value = await globalState.showCommonDialog( + child: OptionsDialog( + title: appLocalizations.colorSchemes, + options: DynamicSchemeVariant.values, + textBuilder: (item) => Intl.message("${item.name}Scheme"), + value: schemeVariant, + ), + ); + if (value == null) { + return; + } + ref.read(themeSettingProvider.notifier).updateState( + (state) { + return state.copyWith( + schemeVariant: value, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final vm3 = ref.watch( + themeSettingProvider.select( + (state) => VM3( + a: state.primaryColor, + b: state.primaryColors, + c: state.schemeVariant, + ), + ), + ); + final primaryColor = vm3.a; + final primaryColors = [null, ...vm3.b]; + final schemeVariant = vm3.c; + return ItemCard( info: Info( label: appLocalizations.themeColor, iconData: Icons.palette, ), + actions: genActions( + [ + if (_removablePrimaryColor == null) + FilledButton( + style: ButtonStyle( + visualDensity: VisualDensity.compact, + ), + onPressed: _handleChangeSchemeVariant, + child: Text(Intl.message("${schemeVariant.name}Scheme")), + ), + _removablePrimaryColor != null + ? FilledButton( + style: ButtonStyle( + visualDensity: VisualDensity.compact, + ), + onPressed: () { + setState(() { + _removablePrimaryColor = null; + }); + }, + child: Text(appLocalizations.cancel), + ) + : IconButton.filledTonal( + iconSize: 20, + padding: EdgeInsets.all(4), + visualDensity: VisualDensity.compact, + onPressed: _handleReset, + icon: Icon(Icons.replay), + ) + ], + space: 8, + ), child: Container( margin: const EdgeInsets.only( left: 16, right: 16, bottom: 16, ), - height: 88, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemBuilder: (_, index) { - final color = primaryColors[index]; - return ColorSchemeBox( - isSelected: color?.toARGB32() == primaryColor, - primaryColor: color, - onPressed: () { - ref.read(themeSettingProvider.notifier).updateState( - (state) => state.copyWith( - primaryColor: color?.toARGB32(), + child: LayoutBuilder( + builder: (_, constraints) { + final columns = _calcColumns(constraints.maxWidth); + final itemWidth = + (constraints.maxWidth - (columns - 1) * 16) / columns; + return Wrap( + spacing: 16, + runSpacing: 16, + children: [ + for (final color in primaryColors) + Container( + clipBehavior: Clip.none, + width: itemWidth, + height: itemWidth, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + EffectGestureDetector( + child: ColorSchemeBox( + isSelected: color == primaryColor, + primaryColor: color != null ? Color(color) : null, + onPressed: () { + ref + .read(themeSettingProvider.notifier) + .updateState( + (state) => state.copyWith( + primaryColor: color, + ), + ); + }, + ), + onLongPress: () { + setState(() { + _removablePrimaryColor = color; + }); + }, + ), + if (_removablePrimaryColor != null && + _removablePrimaryColor == color) + Container( + color: Colors.white.opacity0, + padding: EdgeInsets.all(8), + child: IconButton.filledTonal( + onPressed: _handleDel, + padding: EdgeInsets.all(12), + iconSize: 30, + icon: Icon( + color: context.colorScheme.primary, + Icons.delete, + ), + ), + ), + ], + ), + ), + if (_removablePrimaryColor == null) + Container( + width: itemWidth, + height: itemWidth, + padding: EdgeInsets.all( + 4, + ), + child: IconButton.filledTonal( + onPressed: _handleAdd, + iconSize: 32, + icon: Icon( + color: context.colorScheme.primary, + Icons.add, ), - ); - }, + ), + ) + ], ); }, - separatorBuilder: (_, __) { - return const SizedBox( - width: 16, - ); - }, - itemCount: primaryColors.length, ), ), ); @@ -326,9 +445,14 @@ class _PrueBlackItem extends ConsumerWidget { child: ListItem.switchItem( leading: Icon( Icons.contrast, - color: context.colorScheme.primary, ), - title: Text(appLocalizations.pureBlackMode), + horizontalTitleGap: 12, + title: Text( + appLocalizations.pureBlackMode, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + ), delegate: SwitchDelegate( value: prueBlack, onChanged: (value) { @@ -343,3 +467,66 @@ class _PrueBlackItem extends ConsumerWidget { ); } } + +class _PaletteDialog extends StatefulWidget { + const _PaletteDialog(); + + @override + State<_PaletteDialog> createState() => _PaletteDialogState(); +} + +class _PaletteDialogState extends State<_PaletteDialog> { + final _controller = ValueNotifier(Colors.transparent); + + @override + Widget build(BuildContext context) { + return CommonDialog( + title: appLocalizations.palette, + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(appLocalizations.cancel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(_controller.value.toARGB32()); + }, + child: Text(appLocalizations.confirm), + ), + ], + child: Column( + children: [ + SizedBox( + height: 8, + ), + SizedBox( + width: 250, + height: 250, + child: Palette( + controller: _controller, + ), + ), + SizedBox( + height: 24, + ), + ValueListenableBuilder( + valueListenable: _controller, + builder: (_, color, __) { + return PrimaryColorBox( + primaryColor: color, + child: FilledButton( + onPressed: () {}, + child: Text( + _controller.value.hex, + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart index 9f85d6d..b5dac87 100644 --- a/lib/fragments/tools.dart +++ b/lib/fragments/tools.dart @@ -122,7 +122,7 @@ class _LocaleItem extends ConsumerWidget { final locale = ref.watch(appSettingProvider.select((state) => state.locale)); final subTitle = locale ?? appLocalizations.defaultText; - final currentLocale = other.getLocaleForString(locale); + final currentLocale = utils.getLocaleForString(locale); return ListItem.options( leading: const Icon(Icons.language_outlined), title: Text(appLocalizations.language), diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index d21add9..0d8f281 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -372,5 +372,18 @@ "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?" + "saveTip": "Are you sure you want to save?", + "deleteColorTip": "Are you sure you want to delete the current color?", + "colorExists": "Current color already exists", + "colorSchemes": "Color schemes", + "palette": "Palette", + "tonalSpotScheme": "TonalSpot", + "fidelityScheme": "Fidelity", + "monochromeScheme": "Monochrome", + "neutralScheme": "Neutral", + "vibrantScheme": "Vibrant", + "expressiveScheme": "Expressive", + "contentScheme": "Content", + "rainbowScheme": "Rainbow", + "fruitSaladScheme": "FruitSalad" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_ja.arb b/lib/l10n/arb/intl_ja.arb index 7c64a38..7282952 100644 --- a/lib/l10n/arb/intl_ja.arb +++ b/lib/l10n/arb/intl_ja.arb @@ -372,5 +372,18 @@ "generalDesc": "一般設定を変更", "findProcessModeDesc": "有効化するとパフォーマンスが若干低下します", "tabAnimationDesc": "モバイル表示でのみ有効", - "saveTip": "保存してもよろしいですか?" + "saveTip": "保存してもよろしいですか?", + "deleteColorTip": "現在の色を削除しますか?", + "colorExists": "この色は既に存在します", + "colorSchemes": "カラースキーム", + "palette": "パレット", + "tonalSpotScheme": "トーンスポット", + "fidelityScheme": "ハイファイデリティー", + "monochromeScheme": "モノクローム", + "neutralScheme": "ニュートラル", + "vibrantScheme": "ビブラント", + "expressiveScheme": "エクスプレッシブ", + "contentScheme": "コンテンツテーマ", + "rainbowScheme": "レインボー", + "fruitSaladScheme": "フルーツサラダ" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_ru.arb b/lib/l10n/arb/intl_ru.arb index 5f40f90..132ec01 100644 --- a/lib/l10n/arb/intl_ru.arb +++ b/lib/l10n/arb/intl_ru.arb @@ -372,5 +372,18 @@ "generalDesc": "Изменение общих настроек", "findProcessModeDesc": "При включении возможны небольшие потери производительности", "tabAnimationDesc": "Действительно только в мобильном виде", - "saveTip": "Вы уверены, что хотите сохранить?" + "saveTip": "Вы уверены, что хотите сохранить?", + "deleteColorTip": "Удалить текущий цвет?", + "colorExists": "Этот цвет уже существует", + "colorSchemes": "Цветовые схемы", + "palette": "Палитра", + "tonalSpotScheme": "Тональный акцент", + "fidelityScheme": "Точная передача", + "monochromeScheme": "Монохром", + "neutralScheme": "Нейтральные", + "vibrantScheme": "Яркие", + "expressiveScheme": "Экспрессивные", + "contentScheme": "Контентная тема", + "rainbowScheme": "Радужные", + "fruitSaladScheme": "Фруктовый микс" } \ 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 7b32e0b..00a7749 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -372,5 +372,18 @@ "generalDesc": "修改通用设置", "findProcessModeDesc": "开启后会有一定性能损耗", "tabAnimationDesc": "仅在移动视图中有效", - "saveTip": "确定要保存吗?" + "saveTip": "确定要保存吗?", + "deleteColorTip": "确定删除当前颜色吗?", + "colorExists": "该颜色已存在", + "colorSchemes": "配色方案", + "palette": "调色板", + "tonalSpotScheme": "调性点缀", + "fidelityScheme": "高保真", + "monochromeScheme": "单色", + "neutralScheme": "中性", + "vibrantScheme": "活力", + "expressiveScheme": "表现力", + "contentScheme": "内容主题", + "rainbowScheme": "彩虹", + "fruitSaladScheme": "果缤纷" } diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 8b63e4b..f7b0b08 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -148,6 +148,10 @@ class MessageLookup extends MessageLookupByLibrary { "checking": MessageLookupByLibrary.simpleMessage("Checking..."), "clipboardExport": MessageLookupByLibrary.simpleMessage("Export clipboard"), "clipboardImport": MessageLookupByLibrary.simpleMessage("Clipboard import"), + "colorExists": MessageLookupByLibrary.simpleMessage( + "Current color already exists", + ), + "colorSchemes": MessageLookupByLibrary.simpleMessage("Color schemes"), "columns": MessageLookupByLibrary.simpleMessage("Columns"), "compatible": MessageLookupByLibrary.simpleMessage("Compatibility mode"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( @@ -163,6 +167,7 @@ class MessageLookup extends MessageLookupByLibrary { "contentEmptyTip": MessageLookupByLibrary.simpleMessage( "Content cannot be empty", ), + "contentScheme": MessageLookupByLibrary.simpleMessage("Content"), "copy": MessageLookupByLibrary.simpleMessage("Copy"), "copyEnvVar": MessageLookupByLibrary.simpleMessage( "Copying environment variables", @@ -188,6 +193,9 @@ class MessageLookup extends MessageLookupByLibrary { "delay": MessageLookupByLibrary.simpleMessage("Delay"), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "deleteColorTip": MessageLookupByLibrary.simpleMessage( + "Are you sure you want to delete the current color?", + ), "deleteProfileTip": MessageLookupByLibrary.simpleMessage( "Sure you want to delete the current profile?", ), @@ -234,6 +242,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportFile": MessageLookupByLibrary.simpleMessage("Export file"), "exportLogs": MessageLookupByLibrary.simpleMessage("Export logs"), "exportSuccess": MessageLookupByLibrary.simpleMessage("Export Success"), + "expressiveScheme": MessageLookupByLibrary.simpleMessage("Expressive"), "externalController": MessageLookupByLibrary.simpleMessage( "ExternalController", ), @@ -251,6 +260,7 @@ class MessageLookup extends MessageLookupByLibrary { "Generally use offshore DNS", ), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback filter"), + "fidelityScheme": MessageLookupByLibrary.simpleMessage("Fidelity"), "file": MessageLookupByLibrary.simpleMessage("File"), "fileDesc": MessageLookupByLibrary.simpleMessage("Directly upload profile"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage( @@ -265,6 +275,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "fontFamily": MessageLookupByLibrary.simpleMessage("FontFamily"), "fourColumns": MessageLookupByLibrary.simpleMessage("Four columns"), + "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("FruitSalad"), "general": MessageLookupByLibrary.simpleMessage("General"), "generalDesc": MessageLookupByLibrary.simpleMessage( "Modify general settings", @@ -358,6 +369,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "minutes": MessageLookupByLibrary.simpleMessage("Minutes"), "mode": MessageLookupByLibrary.simpleMessage("Mode"), + "monochromeScheme": MessageLookupByLibrary.simpleMessage("Monochrome"), "months": MessageLookupByLibrary.simpleMessage("Months"), "more": MessageLookupByLibrary.simpleMessage("More"), "name": MessageLookupByLibrary.simpleMessage("Name"), @@ -380,6 +392,7 @@ class MessageLookup extends MessageLookupByLibrary { "Network detection", ), "networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"), + "neutralScheme": MessageLookupByLibrary.simpleMessage("Neutral"), "noData": MessageLookupByLibrary.simpleMessage("No data"), "noHotKey": MessageLookupByLibrary.simpleMessage("No HotKey"), "noIcon": MessageLookupByLibrary.simpleMessage("None"), @@ -436,6 +449,7 @@ class MessageLookup extends MessageLookupByLibrary { "overrideOriginRules": MessageLookupByLibrary.simpleMessage( "Override the original rule", ), + "palette": MessageLookupByLibrary.simpleMessage("Palette"), "password": MessageLookupByLibrary.simpleMessage("Password"), "passwordTip": MessageLookupByLibrary.simpleMessage( "Password cannot be empty", @@ -506,6 +520,7 @@ class MessageLookup extends MessageLookupByLibrary { "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Scan QR code to obtain profile", ), + "rainbowScheme": MessageLookupByLibrary.simpleMessage("Rainbow"), "recovery": MessageLookupByLibrary.simpleMessage("Recovery"), "recoveryAll": MessageLookupByLibrary.simpleMessage("Recovery all data"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage( @@ -623,6 +638,7 @@ class MessageLookup extends MessageLookupByLibrary { "time": MessageLookupByLibrary.simpleMessage("Time"), "tip": MessageLookupByLibrary.simpleMessage("tip"), "toggle": MessageLookupByLibrary.simpleMessage("Toggle"), + "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("TonalSpot"), "tools": MessageLookupByLibrary.simpleMessage("Tools"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "tun": MessageLookupByLibrary.simpleMessage("TUN"), @@ -651,6 +667,7 @@ class MessageLookup extends MessageLookupByLibrary { "valueExists": MessageLookupByLibrary.simpleMessage( "The current value already exists", ), + "vibrantScheme": MessageLookupByLibrary.simpleMessage("Vibrant"), "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 8aa6904..3c22730 100644 --- a/lib/l10n/intl/messages_ja.dart +++ b/lib/l10n/intl/messages_ja.dart @@ -106,6 +106,8 @@ class MessageLookup extends MessageLookupByLibrary { "checking": MessageLookupByLibrary.simpleMessage("確認中..."), "clipboardExport": MessageLookupByLibrary.simpleMessage("クリップボードにエクスポート"), "clipboardImport": MessageLookupByLibrary.simpleMessage("クリップボードからインポート"), + "colorExists": MessageLookupByLibrary.simpleMessage("この色は既に存在します"), + "colorSchemes": MessageLookupByLibrary.simpleMessage("カラースキーム"), "columns": MessageLookupByLibrary.simpleMessage("列"), "compatible": MessageLookupByLibrary.simpleMessage("互換モード"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( @@ -117,6 +119,7 @@ class MessageLookup extends MessageLookupByLibrary { "connectivity": MessageLookupByLibrary.simpleMessage("接続性:"), "content": MessageLookupByLibrary.simpleMessage("内容"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容は必須です"), + "contentScheme": MessageLookupByLibrary.simpleMessage("コンテンツテーマ"), "copy": MessageLookupByLibrary.simpleMessage("コピー"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("環境変数をコピー"), "copyLink": MessageLookupByLibrary.simpleMessage("リンクをコピー"), @@ -138,6 +141,7 @@ class MessageLookup extends MessageLookupByLibrary { "delay": MessageLookupByLibrary.simpleMessage("遅延"), "delaySort": MessageLookupByLibrary.simpleMessage("遅延順"), "delete": MessageLookupByLibrary.simpleMessage("削除"), + "deleteColorTip": MessageLookupByLibrary.simpleMessage("現在の色を削除しますか?"), "deleteProfileTip": MessageLookupByLibrary.simpleMessage( "現在のプロファイルを削除しますか?", ), @@ -172,6 +176,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportFile": MessageLookupByLibrary.simpleMessage("ファイルをエクスポート"), "exportLogs": MessageLookupByLibrary.simpleMessage("ログをエクスポート"), "exportSuccess": MessageLookupByLibrary.simpleMessage("エクスポート成功"), + "expressiveScheme": MessageLookupByLibrary.simpleMessage("エクスプレッシブ"), "externalController": MessageLookupByLibrary.simpleMessage("外部コントローラー"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( "有効化するとClashコアをポート9090で制御可能", @@ -183,6 +188,7 @@ class MessageLookup extends MessageLookupByLibrary { "fallback": MessageLookupByLibrary.simpleMessage("フォールバック"), "fallbackDesc": MessageLookupByLibrary.simpleMessage("通常はオフショアDNSを使用"), "fallbackFilter": MessageLookupByLibrary.simpleMessage("フォールバックフィルター"), + "fidelityScheme": MessageLookupByLibrary.simpleMessage("ハイファイデリティー"), "file": MessageLookupByLibrary.simpleMessage("ファイル"), "fileDesc": MessageLookupByLibrary.simpleMessage("プロファイルを直接アップロード"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage( @@ -195,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "fontFamily": MessageLookupByLibrary.simpleMessage("フォントファミリー"), "fourColumns": MessageLookupByLibrary.simpleMessage("4列"), + "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("フルーツサラダ"), "general": MessageLookupByLibrary.simpleMessage("一般"), "generalDesc": MessageLookupByLibrary.simpleMessage("一般設定を変更"), "geoData": MessageLookupByLibrary.simpleMessage("地域データ"), @@ -258,6 +265,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "minutes": MessageLookupByLibrary.simpleMessage("分"), "mode": MessageLookupByLibrary.simpleMessage("モード"), + "monochromeScheme": MessageLookupByLibrary.simpleMessage("モノクローム"), "months": MessageLookupByLibrary.simpleMessage("月"), "more": MessageLookupByLibrary.simpleMessage("詳細"), "name": MessageLookupByLibrary.simpleMessage("名前"), @@ -272,6 +280,7 @@ class MessageLookup extends MessageLookupByLibrary { "networkDesc": MessageLookupByLibrary.simpleMessage("ネットワーク関連設定の変更"), "networkDetection": MessageLookupByLibrary.simpleMessage("ネットワーク検出"), "networkSpeed": MessageLookupByLibrary.simpleMessage("ネットワーク速度"), + "neutralScheme": MessageLookupByLibrary.simpleMessage("ニュートラル"), "noData": MessageLookupByLibrary.simpleMessage("データなし"), "noHotKey": MessageLookupByLibrary.simpleMessage("ホットキーなし"), "noIcon": MessageLookupByLibrary.simpleMessage("なし"), @@ -314,6 +323,7 @@ class MessageLookup extends MessageLookupByLibrary { "有効化するとプロファイルのDNS設定を上書き", ), "overrideOriginRules": MessageLookupByLibrary.simpleMessage("元のルールを上書き"), + "palette": MessageLookupByLibrary.simpleMessage("パレット"), "password": MessageLookupByLibrary.simpleMessage("パスワード"), "passwordTip": MessageLookupByLibrary.simpleMessage("パスワードは必須です"), "paste": MessageLookupByLibrary.simpleMessage("貼り付け"), @@ -370,6 +380,7 @@ class MessageLookup extends MessageLookupByLibrary { "pureBlackMode": MessageLookupByLibrary.simpleMessage("純黒モード"), "qrcode": MessageLookupByLibrary.simpleMessage("QRコード"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("QRコードをスキャンしてプロファイルを取得"), + "rainbowScheme": MessageLookupByLibrary.simpleMessage("レインボー"), "recovery": MessageLookupByLibrary.simpleMessage("復元"), "recoveryAll": MessageLookupByLibrary.simpleMessage("全データ復元"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("プロファイルのみ復元"), @@ -461,6 +472,7 @@ class MessageLookup extends MessageLookupByLibrary { "time": MessageLookupByLibrary.simpleMessage("時間"), "tip": MessageLookupByLibrary.simpleMessage("ヒント"), "toggle": MessageLookupByLibrary.simpleMessage("トグル"), + "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("トーンスポット"), "tools": MessageLookupByLibrary.simpleMessage("ツール"), "trafficUsage": MessageLookupByLibrary.simpleMessage("トラフィック使用量"), "tun": MessageLookupByLibrary.simpleMessage("TUN"), @@ -483,6 +495,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSystemHosts": MessageLookupByLibrary.simpleMessage("システムホストを使用"), "value": MessageLookupByLibrary.simpleMessage("値"), "valueExists": MessageLookupByLibrary.simpleMessage("現在の値は既に存在します"), + "vibrantScheme": 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 1a0e7eb..b53f49a 100644 --- a/lib/l10n/intl/messages_ru.dart +++ b/lib/l10n/intl/messages_ru.dart @@ -154,6 +154,10 @@ class MessageLookup extends MessageLookupByLibrary { "clipboardImport": MessageLookupByLibrary.simpleMessage( "Импорт из буфера обмена", ), + "colorExists": MessageLookupByLibrary.simpleMessage( + "Этот цвет уже существует", + ), + "colorSchemes": MessageLookupByLibrary.simpleMessage("Цветовые схемы"), "columns": MessageLookupByLibrary.simpleMessage("Столбцы"), "compatible": MessageLookupByLibrary.simpleMessage("Режим совместимости"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( @@ -169,6 +173,7 @@ class MessageLookup extends MessageLookupByLibrary { "contentEmptyTip": MessageLookupByLibrary.simpleMessage( "Содержание не может быть пустым", ), + "contentScheme": MessageLookupByLibrary.simpleMessage("Контентная тема"), "copy": MessageLookupByLibrary.simpleMessage("Копировать"), "copyEnvVar": MessageLookupByLibrary.simpleMessage( "Копирование переменных окружения", @@ -196,6 +201,9 @@ class MessageLookup extends MessageLookupByLibrary { "delay": MessageLookupByLibrary.simpleMessage("Задержка"), "delaySort": MessageLookupByLibrary.simpleMessage("Сортировка по задержке"), "delete": MessageLookupByLibrary.simpleMessage("Удалить"), + "deleteColorTip": MessageLookupByLibrary.simpleMessage( + "Удалить текущий цвет?", + ), "deleteProfileTip": MessageLookupByLibrary.simpleMessage( "Вы уверены, что хотите удалить текущий профиль?", ), @@ -248,6 +256,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportFile": MessageLookupByLibrary.simpleMessage("Экспорт файла"), "exportLogs": MessageLookupByLibrary.simpleMessage("Экспорт логов"), "exportSuccess": MessageLookupByLibrary.simpleMessage("Экспорт успешен"), + "expressiveScheme": MessageLookupByLibrary.simpleMessage("Экспрессивные"), "externalController": MessageLookupByLibrary.simpleMessage( "Внешний контроллер", ), @@ -267,6 +276,7 @@ class MessageLookup extends MessageLookupByLibrary { "fallbackFilter": MessageLookupByLibrary.simpleMessage( "Фильтр резервного DNS", ), + "fidelityScheme": MessageLookupByLibrary.simpleMessage("Точная передача"), "file": MessageLookupByLibrary.simpleMessage("Файл"), "fileDesc": MessageLookupByLibrary.simpleMessage("Прямая загрузка профиля"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage( @@ -283,6 +293,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "fontFamily": MessageLookupByLibrary.simpleMessage("Семейство шрифтов"), "fourColumns": MessageLookupByLibrary.simpleMessage("Четыре столбца"), + "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("Фруктовый микс"), "general": MessageLookupByLibrary.simpleMessage("Общие"), "generalDesc": MessageLookupByLibrary.simpleMessage( "Изменение общих настроек", @@ -384,6 +395,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "minutes": MessageLookupByLibrary.simpleMessage("Минут"), "mode": MessageLookupByLibrary.simpleMessage("Режим"), + "monochromeScheme": MessageLookupByLibrary.simpleMessage("Монохром"), "months": MessageLookupByLibrary.simpleMessage("Месяцев"), "more": MessageLookupByLibrary.simpleMessage("Еще"), "name": MessageLookupByLibrary.simpleMessage("Имя"), @@ -406,6 +418,7 @@ class MessageLookup extends MessageLookupByLibrary { "Обнаружение сети", ), "networkSpeed": MessageLookupByLibrary.simpleMessage("Скорость сети"), + "neutralScheme": MessageLookupByLibrary.simpleMessage("Нейтральные"), "noData": MessageLookupByLibrary.simpleMessage("Нет данных"), "noHotKey": MessageLookupByLibrary.simpleMessage("Нет горячей клавиши"), "noIcon": MessageLookupByLibrary.simpleMessage("Нет иконки"), @@ -466,6 +479,7 @@ class MessageLookup extends MessageLookupByLibrary { "overrideOriginRules": MessageLookupByLibrary.simpleMessage( "Переопределить оригинальное правило", ), + "palette": MessageLookupByLibrary.simpleMessage("Палитра"), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "passwordTip": MessageLookupByLibrary.simpleMessage( "Пароль не может быть пустым", @@ -538,6 +552,7 @@ class MessageLookup extends MessageLookupByLibrary { "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Сканируйте QR-код для получения профиля", ), + "rainbowScheme": MessageLookupByLibrary.simpleMessage("Радужные"), "recovery": MessageLookupByLibrary.simpleMessage("Восстановление"), "recoveryAll": MessageLookupByLibrary.simpleMessage( "Восстановить все данные", @@ -661,6 +676,7 @@ class MessageLookup extends MessageLookupByLibrary { "time": MessageLookupByLibrary.simpleMessage("Время"), "tip": MessageLookupByLibrary.simpleMessage("подсказка"), "toggle": MessageLookupByLibrary.simpleMessage("Переключить"), + "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("Тональный акцент"), "tools": MessageLookupByLibrary.simpleMessage("Инструменты"), "trafficUsage": MessageLookupByLibrary.simpleMessage( "Использование трафика", @@ -695,6 +711,7 @@ class MessageLookup extends MessageLookupByLibrary { "valueExists": MessageLookupByLibrary.simpleMessage( "Текущее значение уже существует", ), + "vibrantScheme": 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 5135f85..d9b875f 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -96,6 +96,8 @@ class MessageLookup extends MessageLookupByLibrary { "checking": MessageLookupByLibrary.simpleMessage("检测中..."), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), + "colorExists": MessageLookupByLibrary.simpleMessage("该颜色已存在"), + "colorSchemes": MessageLookupByLibrary.simpleMessage("配色方案"), "columns": MessageLookupByLibrary.simpleMessage("列数"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( @@ -107,6 +109,7 @@ class MessageLookup extends MessageLookupByLibrary { "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), "content": MessageLookupByLibrary.simpleMessage("内容"), "contentEmptyTip": MessageLookupByLibrary.simpleMessage("内容不能为空"), + "contentScheme": MessageLookupByLibrary.simpleMessage("内容主题"), "copy": MessageLookupByLibrary.simpleMessage("复制"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"), "copyLink": MessageLookupByLibrary.simpleMessage("复制链接"), @@ -126,6 +129,7 @@ class MessageLookup extends MessageLookupByLibrary { "delay": MessageLookupByLibrary.simpleMessage("延迟"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delete": MessageLookupByLibrary.simpleMessage("删除"), + "deleteColorTip": MessageLookupByLibrary.simpleMessage("确定删除当前颜色吗?"), "deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"), "deleteRuleTip": MessageLookupByLibrary.simpleMessage("确定要删除选中的规则吗?"), "desc": MessageLookupByLibrary.simpleMessage( @@ -156,6 +160,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportFile": MessageLookupByLibrary.simpleMessage("导出文件"), "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"), + "expressiveScheme": MessageLookupByLibrary.simpleMessage("表现力"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage( "开启后将可以通过9090端口控制Clash内核", @@ -167,6 +172,7 @@ class MessageLookup extends MessageLookupByLibrary { "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"), + "fidelityScheme": MessageLookupByLibrary.simpleMessage("高保真"), "file": MessageLookupByLibrary.simpleMessage("文件"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"), @@ -175,6 +181,7 @@ class MessageLookup extends MessageLookupByLibrary { "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后会有一定性能损耗"), "fontFamily": MessageLookupByLibrary.simpleMessage("字体"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"), + "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("果缤纷"), "general": MessageLookupByLibrary.simpleMessage("常规"), "generalDesc": MessageLookupByLibrary.simpleMessage("修改通用设置"), "geoData": MessageLookupByLibrary.simpleMessage("地理数据"), @@ -230,6 +237,7 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), "minutes": MessageLookupByLibrary.simpleMessage("分钟"), "mode": MessageLookupByLibrary.simpleMessage("模式"), + "monochromeScheme": MessageLookupByLibrary.simpleMessage("单色"), "months": MessageLookupByLibrary.simpleMessage("月"), "more": MessageLookupByLibrary.simpleMessage("更多"), "name": MessageLookupByLibrary.simpleMessage("名称"), @@ -242,6 +250,7 @@ class MessageLookup extends MessageLookupByLibrary { "networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), + "neutralScheme": MessageLookupByLibrary.simpleMessage("中性"), "noData": MessageLookupByLibrary.simpleMessage("暂无数据"), "noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"), "noIcon": MessageLookupByLibrary.simpleMessage("无图标"), @@ -276,6 +285,7 @@ class MessageLookup extends MessageLookupByLibrary { "overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"), "overrideOriginRules": MessageLookupByLibrary.simpleMessage("覆盖原始规则"), + "palette": MessageLookupByLibrary.simpleMessage("调色板"), "password": MessageLookupByLibrary.simpleMessage("密码"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"), @@ -324,6 +334,7 @@ class MessageLookup extends MessageLookupByLibrary { "pureBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), + "rainbowScheme": MessageLookupByLibrary.simpleMessage("彩虹"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), @@ -405,6 +416,7 @@ class MessageLookup extends MessageLookupByLibrary { "time": MessageLookupByLibrary.simpleMessage("时间"), "tip": MessageLookupByLibrary.simpleMessage("提示"), "toggle": MessageLookupByLibrary.simpleMessage("切换"), + "tonalSpotScheme": MessageLookupByLibrary.simpleMessage("调性点缀"), "tools": MessageLookupByLibrary.simpleMessage("工具"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), @@ -425,6 +437,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "value": MessageLookupByLibrary.simpleMessage("值"), "valueExists": MessageLookupByLibrary.simpleMessage("当前值已存在"), + "vibrantScheme": 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 a9f505e..c6a4512 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2904,6 +2904,106 @@ class AppLocalizations { args: [], ); } + + /// `Are you sure you want to delete the current color?` + String get deleteColorTip { + return Intl.message( + 'Are you sure you want to delete the current color?', + name: 'deleteColorTip', + desc: '', + args: [], + ); + } + + /// `Current color already exists` + String get colorExists { + return Intl.message( + 'Current color already exists', + name: 'colorExists', + desc: '', + args: [], + ); + } + + /// `Color schemes` + String get colorSchemes { + return Intl.message( + 'Color schemes', + name: 'colorSchemes', + desc: '', + args: [], + ); + } + + /// `Palette` + String get palette { + return Intl.message('Palette', name: 'palette', desc: '', args: []); + } + + /// `TonalSpot` + String get tonalSpotScheme { + return Intl.message( + 'TonalSpot', + name: 'tonalSpotScheme', + desc: '', + args: [], + ); + } + + /// `Fidelity` + String get fidelityScheme { + return Intl.message('Fidelity', name: 'fidelityScheme', desc: '', args: []); + } + + /// `Monochrome` + String get monochromeScheme { + return Intl.message( + 'Monochrome', + name: 'monochromeScheme', + desc: '', + args: [], + ); + } + + /// `Neutral` + String get neutralScheme { + return Intl.message('Neutral', name: 'neutralScheme', desc: '', args: []); + } + + /// `Vibrant` + String get vibrantScheme { + return Intl.message('Vibrant', name: 'vibrantScheme', desc: '', args: []); + } + + /// `Expressive` + String get expressiveScheme { + return Intl.message( + 'Expressive', + name: 'expressiveScheme', + desc: '', + args: [], + ); + } + + /// `Content` + String get contentScheme { + return Intl.message('Content', name: 'contentScheme', desc: '', args: []); + } + + /// `Rainbow` + String get rainbowScheme { + return Intl.message('Rainbow', name: 'rainbowScheme', desc: '', args: []); + } + + /// `FruitSalad` + String get fruitSaladScheme { + return Intl.message( + 'FruitSalad', + name: 'fruitSaladScheme', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 91e9edf..3d2e6fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; -import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/core.dart'; import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/vpn.dart'; @@ -17,7 +17,6 @@ import 'application.dart'; import 'clash/core.dart'; import 'clash/lib.dart'; import 'common/common.dart'; -import 'models/models.dart'; Future main() async { globalState.isService = false; @@ -47,7 +46,6 @@ Future _service(List flags) async { onStop: () async { await app?.tip(appLocalizations.stopVpn); clashLibHandler.stopListener(); - clashLibHandler.stopTun(); await vpn?.stop(); exit(0); }, @@ -64,43 +62,11 @@ Future _service(List flags) async { vpn?.addListener( _VpnListenerWithService( - onStarted: (int fd) { - commonPrint.log("vpn started fd: $fd"); - final time = clashLibHandler.startTun(fd); - commonPrint.log("vpn start tun time: $time"); - }, onDnsChanged: (String dns) { clashLibHandler.updateDns(dns); }, ), ); - - final invokeReceiverPort = ReceivePort(); - - clashLibHandler.attachInvokePort( - invokeReceiverPort.sendPort.nativePort, - ); - - invokeReceiverPort.listen( - (message) async { - final invokeMessage = InvokeMessage.fromJson(json.decode(message)); - switch (invokeMessage.type) { - case InvokeMessageType.protect: - final fd = Fd.fromJson(invokeMessage.data); - await vpn?.setProtect(fd.value); - clashLibHandler.setFdMap(fd.id); - case InvokeMessageType.process: - final process = ProcessData.fromJson(invokeMessage.data); - final processName = await vpn?.resolverProcess(process) ?? ""; - clashLibHandler.setProcessMap( - ProcessMapItem( - id: process.id, - value: processName, - ), - ); - } - }, - ); if (!quickStart) { _handleMainIpc(clashLibHandler); } else { @@ -108,9 +74,13 @@ Future _service(List flags) async { await ClashCore.initGeo(); app?.tip(appLocalizations.startVpn); final homeDirPath = await appPath.homeDirPath; + final version = await system.version; clashLibHandler .quickStart( - homeDirPath, + InitParams( + homeDir: homeDirPath, + version: version, + ), globalState.getUpdateConfigParams(), globalState.getCoreState(), ) @@ -165,20 +135,11 @@ class _TileListenerWithService with TileListener { @immutable class _VpnListenerWithService with VpnListener { - final Function(int fd) _onStarted; final Function(String dns) _onDnsChanged; const _VpnListenerWithService({ - required Function(int fd) onStarted, required Function(String dns) onDnsChanged, - }) : _onStarted = onStarted, - _onDnsChanged = onDnsChanged; - - @override - void onStarted(int fd) { - super.onStarted(fd); - _onStarted(fd); - } + }) : _onDnsChanged = onDnsChanged; @override void onDnsChanged(String dns) { diff --git a/lib/manager/app_state_manager.dart b/lib/manager/app_state_manager.dart index 5724b69..e6ec465 100644 --- a/lib/manager/app_state_manager.dart +++ b/lib/manager/app_state_manager.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class AppStateManager extends StatefulWidget { @@ -21,7 +22,6 @@ class _AppStateManagerState extends State @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); } @@ -33,10 +33,10 @@ class _AppStateManagerState extends State @override Future didChangeAppLifecycleState(AppLifecycleState state) async { + commonPrint.log("$state"); if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { globalState.appController.savePreferences(); - render?.pause(); } else { render?.resume(); } @@ -70,6 +70,15 @@ class AppEnvManager extends StatelessWidget { @override Widget build(BuildContext context) { + if (kDebugMode) { + if (globalState.isPre) { + return Banner( + message: 'DEBUG', + location: BannerLocation.topEnd, + child: child, + ); + } + } if (globalState.isPre) { return Banner( message: 'PRE', diff --git a/lib/manager/message_manager.dart b/lib/manager/message_manager.dart index 11975b7..cbf6df8 100644 --- a/lib/manager/message_manager.dart +++ b/lib/manager/message_manager.dart @@ -36,7 +36,7 @@ class MessageManagerState extends State { Future message(String text) async { final commonMessage = CommonMessage( - id: other.uuidV4, + id: utils.uuidV4, text: text, ); _bufferMessages.add(commonMessage); @@ -91,7 +91,7 @@ class MessageManagerState extends State { ), elevation: 10, margin: EdgeInsets.only( - top: kToolbarHeight, + top: kToolbarHeight + 8, left: 12, right: 12, ), diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index feff26a..e2790f1 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -60,15 +60,10 @@ class _WindowContainerState extends ConsumerState @override void onWindowFocus() { super.onWindowFocus(); + commonPrint.log("focus"); render?.resume(); } - @override - void onWindowBlur() { - super.onWindowBlur(); - render?.pause(); - } - @override Future onShouldTerminate() async { await globalState.appController.handleExit(); @@ -102,6 +97,8 @@ class _WindowContainerState extends ConsumerState @override void onWindowMinimize() async { globalState.appController.savePreferencesDebounce(); + commonPrint.log("minimize"); + render?.pause(); super.onWindowMinimize(); } diff --git a/lib/models/app.dart b/lib/models/app.dart index d7f10b6..f2d2ef9 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -31,11 +31,12 @@ class AppState with _$AppState { required FixedList logs, required FixedList traffics, required Traffic totalTraffic, + @Default(false) bool needApply, }) = _AppState; } extension AppStateExt on AppState { - ViewMode get viewMode => other.getViewMode(viewSize.width); + ViewMode get viewMode => utils.getViewMode(viewSize.width); bool get isStart => runTime != null; } diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 3c84864..9b40834 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -361,7 +361,7 @@ class Rule with _$Rule { factory Rule.value(String value) { return Rule( value: value, - id: other.uuidV4, + id: utils.uuidV4, ); } @@ -426,12 +426,12 @@ class ClashConfig with _$ClashConfig { @Default(defaultMixedPort) @JsonKey(name: "mixed-port") int mixedPort, @Default(Mode.rule) Mode mode, @Default(false) @JsonKey(name: "allow-lan") bool allowLan, - @Default(LogLevel.info) @JsonKey(name: "log-level") LogLevel logLevel, + @Default(LogLevel.error) @JsonKey(name: "log-level") LogLevel logLevel, @Default(false) bool ipv6, @Default(FindProcessMode.off) @JsonKey( name: "find-process-mode", - unknownEnumValue: FindProcessMode.off, + unknownEnumValue: FindProcessMode.always, ) FindProcessMode findProcessMode, @Default(defaultKeepAliveInterval) diff --git a/lib/models/common.dart b/lib/models/common.dart index 10c60f0..df244c7 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -369,21 +369,32 @@ class ColorSchemes with _$ColorSchemes { } extension ColorSchemesExt on ColorSchemes { - ColorScheme getColorSchemeForBrightness(Brightness? brightness) { + ColorScheme getColorSchemeForBrightness( + Brightness brightness, + DynamicSchemeVariant schemeVariant, + ) { if (brightness == Brightness.dark) { return darkColorScheme != null ? ColorScheme.fromSeed( seedColor: darkColorScheme!.primary, brightness: Brightness.dark, + dynamicSchemeVariant: schemeVariant, ) : ColorScheme.fromSeed( - seedColor: defaultPrimaryColor, + seedColor: Color(defaultPrimaryColor), brightness: Brightness.dark, + dynamicSchemeVariant: schemeVariant, ); } return lightColorScheme != null - ? ColorScheme.fromSeed(seedColor: lightColorScheme!.primary,dynamicSchemeVariant: DynamicSchemeVariant.vibrant) - : ColorScheme.fromSeed(seedColor: defaultPrimaryColor); + ? ColorScheme.fromSeed( + seedColor: lightColorScheme!.primary, + dynamicSchemeVariant: schemeVariant, + ) + : ColorScheme.fromSeed( + seedColor: Color(defaultPrimaryColor), + dynamicSchemeVariant: schemeVariant, + ); } } @@ -500,4 +511,4 @@ class PopupMenuItemData { final VoidCallback? onPressed; final IconData? icon; final PopupMenuItemType? type; -} \ No newline at end of file +} diff --git a/lib/models/config.dart b/lib/models/config.dart index bf5a5f3..18fed53 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -8,6 +8,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'models.dart'; part 'generated/config.freezed.dart'; + part 'generated/config.g.dart'; const defaultBypassDomain = [ @@ -36,10 +37,7 @@ const defaultNetworkProps = NetworkProps(); const defaultProxiesStyle = ProxiesStyle(); const defaultWindowProps = WindowProps(); const defaultAccessControl = AccessControl(); -final defaultThemeProps = ThemeProps().copyWith( - primaryColor: defaultPrimaryColor.toARGB32(), - themeMode: ThemeMode.dark, -); +const defaultThemeProps = ThemeProps(); const List defaultDashboardWidgets = [ DashboardWidget.networkSpeed, @@ -75,7 +73,7 @@ class AppSettingProps with _$AppSettingProps { @Default(false) bool autoLaunch, @Default(false) bool silentLaunch, @Default(false) bool autoRun, - @Default(false) bool openLogs, + @Default(true) bool openLogs, @Default(true) bool closeConnections, @Default(defaultTestUrl) String testUrl, @Default(true) bool isAnimateToPage, @@ -142,7 +140,7 @@ class VpnProps with _$VpnProps { }) = _VpnProps; factory VpnProps.fromJson(Map? json) => - json == null ? const VpnProps() : _$VpnPropsFromJson(json); + json == null ? defaultVpnProps : _$VpnPropsFromJson(json); } @freezed @@ -175,24 +173,15 @@ class ProxiesStyle with _$ProxiesStyle { @freezed class ThemeProps with _$ThemeProps { const factory ThemeProps({ - int? primaryColor, - @Default(ThemeMode.system) ThemeMode themeMode, + @Default(defaultPrimaryColor) int? primaryColor, + @Default(defaultPrimaryColors) List primaryColors, + @Default(ThemeMode.dark) ThemeMode themeMode, + @Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant, @Default(false) bool pureBlack, }) = _ThemeProps; - factory ThemeProps.fromJson(Map json) => - _$ThemePropsFromJson(json); - - factory ThemeProps.safeFromJson(Map? json) { - if (json == null) { - return defaultThemeProps; - } - try { - return ThemeProps.fromJson(json); - } catch (_) { - return defaultThemeProps; - } - } + factory ThemeProps.fromJson(Map? json) => + json == null ? defaultThemeProps : _$ThemePropsFromJson(json); } @freezed @@ -208,7 +197,7 @@ class Config with _$Config { DAV? dav, @Default(defaultNetworkProps) NetworkProps networkProps, @Default(defaultVpnProps) VpnProps vpnProps, - @JsonKey(fromJson: ThemeProps.safeFromJson) required ThemeProps themeProps, + @Default(defaultThemeProps) ThemeProps themeProps, @Default(defaultProxiesStyle) ProxiesStyle proxiesStyle, @Default(defaultWindowProps) WindowProps windowProps, @Default(defaultClashConfig) ClashConfig patchClashConfig, diff --git a/lib/models/core.dart b/lib/models/core.dart index 04373c7..934ce0d 100644 --- a/lib/models/core.dart +++ b/lib/models/core.dart @@ -82,6 +82,17 @@ class UpdateConfigParams with _$UpdateConfigParams { _$UpdateConfigParamsFromJson(json); } +@freezed +class InitParams with _$InitParams { + const factory InitParams({ + @JsonKey(name: "home-dir") required String homeDir, + required int version, + }) = _InitParams; + + factory InitParams.fromJson(Map json) => + _$InitParamsFromJson(json); +} + @freezed class ChangeProxyParams with _$ChangeProxyParams { const factory ChangeProxyParams({ diff --git a/lib/models/generated/app.freezed.dart b/lib/models/generated/app.freezed.dart index 522245d..0623cb7 100644 --- a/lib/models/generated/app.freezed.dart +++ b/lib/models/generated/app.freezed.dart @@ -35,6 +35,7 @@ mixin _$AppState { FixedList get logs => throw _privateConstructorUsedError; FixedList get traffics => throw _privateConstructorUsedError; Traffic get totalTraffic => throw _privateConstructorUsedError; + bool get needApply => throw _privateConstructorUsedError; /// Create a copy of AppState /// with the given fields replaced by the non-null parameter values. @@ -66,7 +67,8 @@ abstract class $AppStateCopyWith<$Res> { int version, FixedList logs, FixedList traffics, - Traffic totalTraffic}); + Traffic totalTraffic, + bool needApply}); $ColorSchemesCopyWith<$Res> get colorSchemes; } @@ -104,6 +106,7 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> Object? logs = null, Object? traffics = null, Object? totalTraffic = null, + Object? needApply = null, }) { return _then(_value.copyWith( isInit: null == isInit @@ -178,6 +181,10 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState> ? _value.totalTraffic : totalTraffic // ignore: cast_nullable_to_non_nullable as Traffic, + needApply: null == needApply + ? _value.needApply + : needApply // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -218,7 +225,8 @@ abstract class _$$AppStateImplCopyWith<$Res> int version, FixedList logs, FixedList traffics, - Traffic totalTraffic}); + Traffic totalTraffic, + bool needApply}); @override $ColorSchemesCopyWith<$Res> get colorSchemes; @@ -255,6 +263,7 @@ class __$$AppStateImplCopyWithImpl<$Res> Object? logs = null, Object? traffics = null, Object? totalTraffic = null, + Object? needApply = null, }) { return _then(_$AppStateImpl( isInit: null == isInit @@ -329,6 +338,10 @@ class __$$AppStateImplCopyWithImpl<$Res> ? _value.totalTraffic : totalTraffic // ignore: cast_nullable_to_non_nullable as Traffic, + needApply: null == needApply + ? _value.needApply + : needApply // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -354,7 +367,8 @@ class _$AppStateImpl implements _AppState { required this.version, required this.logs, required this.traffics, - required this.totalTraffic}) + required this.totalTraffic, + this.needApply = false}) : _packages = packages, _delayMap = delayMap, _groups = groups, @@ -429,10 +443,13 @@ class _$AppStateImpl implements _AppState { final FixedList traffics; @override final Traffic totalTraffic; + @override + @JsonKey() + final bool needApply; @override String toString() { - return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, colorSchemes: $colorSchemes, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic)'; + return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, colorSchemes: $colorSchemes, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, needApply: $needApply)'; } @override @@ -466,30 +483,34 @@ class _$AppStateImpl implements _AppState { (identical(other.traffics, traffics) || other.traffics == traffics) && (identical(other.totalTraffic, totalTraffic) || - other.totalTraffic == totalTraffic)); + other.totalTraffic == totalTraffic) && + (identical(other.needApply, needApply) || + other.needApply == needApply)); } @override - int get hashCode => Object.hash( - runtimeType, - isInit, - pageLabel, - const DeepCollectionEquality().hash(_packages), - colorSchemes, - sortNum, - viewSize, - const DeepCollectionEquality().hash(_delayMap), - const DeepCollectionEquality().hash(_groups), - checkIpNum, - brightness, - runTime, - const DeepCollectionEquality().hash(_providers), - localIp, - requests, - version, - logs, - traffics, - totalTraffic); + int get hashCode => Object.hashAll([ + runtimeType, + isInit, + pageLabel, + const DeepCollectionEquality().hash(_packages), + colorSchemes, + sortNum, + viewSize, + const DeepCollectionEquality().hash(_delayMap), + const DeepCollectionEquality().hash(_groups), + checkIpNum, + brightness, + runTime, + const DeepCollectionEquality().hash(_providers), + localIp, + requests, + version, + logs, + traffics, + totalTraffic, + needApply + ]); /// Create a copy of AppState /// with the given fields replaced by the non-null parameter values. @@ -519,7 +540,8 @@ abstract class _AppState implements AppState { required final int version, required final FixedList logs, required final FixedList traffics, - required final Traffic totalTraffic}) = _$AppStateImpl; + required final Traffic totalTraffic, + final bool needApply}) = _$AppStateImpl; @override bool get isInit; @@ -557,6 +579,8 @@ abstract class _AppState implements AppState { FixedList get traffics; @override Traffic get totalTraffic; + @override + bool get needApply; /// Create a copy of AppState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/generated/clash_config.freezed.dart b/lib/models/generated/clash_config.freezed.dart index 1f851b2..43c9b43 100644 --- a/lib/models/generated/clash_config.freezed.dart +++ b/lib/models/generated/clash_config.freezed.dart @@ -2832,7 +2832,7 @@ mixin _$ClashConfig { @JsonKey(name: "log-level") LogLevel get logLevel => throw _privateConstructorUsedError; bool get ipv6 => throw _privateConstructorUsedError; - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.always) FindProcessMode get findProcessMode => throw _privateConstructorUsedError; @JsonKey(name: "keep-alive-interval") int get keepAliveInterval => throw _privateConstructorUsedError; @@ -2880,7 +2880,8 @@ abstract class $ClashConfigCopyWith<$Res> { @JsonKey(name: "allow-lan") bool allowLan, @JsonKey(name: "log-level") LogLevel logLevel, bool ipv6, - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey( + name: "find-process-mode", unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: "keep-alive-interval") int keepAliveInterval, @JsonKey(name: "unified-delay") bool unifiedDelay, @@ -3057,7 +3058,8 @@ abstract class _$$ClashConfigImplCopyWith<$Res> @JsonKey(name: "allow-lan") bool allowLan, @JsonKey(name: "log-level") LogLevel logLevel, bool ipv6, - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey( + name: "find-process-mode", unknownEnumValue: FindProcessMode.always) FindProcessMode findProcessMode, @JsonKey(name: "keep-alive-interval") int keepAliveInterval, @JsonKey(name: "unified-delay") bool unifiedDelay, @@ -3198,9 +3200,10 @@ class _$ClashConfigImpl implements _ClashConfig { {@JsonKey(name: "mixed-port") this.mixedPort = defaultMixedPort, this.mode = Mode.rule, @JsonKey(name: "allow-lan") this.allowLan = false, - @JsonKey(name: "log-level") this.logLevel = LogLevel.info, + @JsonKey(name: "log-level") this.logLevel = LogLevel.error, this.ipv6 = false, - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey( + name: "find-process-mode", unknownEnumValue: FindProcessMode.always) this.findProcessMode = FindProcessMode.off, @JsonKey(name: "keep-alive-interval") this.keepAliveInterval = defaultKeepAliveInterval, @@ -3242,7 +3245,7 @@ class _$ClashConfigImpl implements _ClashConfig { @JsonKey() final bool ipv6; @override - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.always) final FindProcessMode findProcessMode; @override @JsonKey(name: "keep-alive-interval") @@ -3385,7 +3388,8 @@ abstract class _ClashConfig implements ClashConfig { @JsonKey(name: "allow-lan") final bool allowLan, @JsonKey(name: "log-level") final LogLevel logLevel, final bool ipv6, - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey( + name: "find-process-mode", unknownEnumValue: FindProcessMode.always) final FindProcessMode findProcessMode, @JsonKey(name: "keep-alive-interval") final int keepAliveInterval, @JsonKey(name: "unified-delay") final bool unifiedDelay, @@ -3419,7 +3423,7 @@ abstract class _ClashConfig implements ClashConfig { @override bool get ipv6; @override - @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.off) + @JsonKey(name: "find-process-mode", unknownEnumValue: FindProcessMode.always) FindProcessMode get findProcessMode; @override @JsonKey(name: "keep-alive-interval") diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 61208c3..954250b 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -268,11 +268,11 @@ _$ClashConfigImpl _$$ClashConfigImplFromJson(Map json) => mode: $enumDecodeNullable(_$ModeEnumMap, json['mode']) ?? Mode.rule, allowLan: json['allow-lan'] as bool? ?? false, logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? - LogLevel.info, + LogLevel.error, ipv6: json['ipv6'] as bool? ?? false, findProcessMode: $enumDecodeNullable( _$FindProcessModeEnumMap, json['find-process-mode'], - unknownValue: FindProcessMode.off) ?? + unknownValue: FindProcessMode.always) ?? FindProcessMode.off, keepAliveInterval: (json['keep-alive-interval'] as num?)?.toInt() ?? defaultKeepAliveInterval, diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 069eb4f..384d607 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -301,7 +301,7 @@ class _$AppSettingPropsImpl implements _AppSettingProps { this.autoLaunch = false, this.silentLaunch = false, this.autoRun = false, - this.openLogs = false, + this.openLogs = true, this.closeConnections = true, this.testUrl = defaultTestUrl, this.isAnimateToPage = true, @@ -1724,7 +1724,9 @@ ThemeProps _$ThemePropsFromJson(Map json) { /// @nodoc mixin _$ThemeProps { int? get primaryColor => throw _privateConstructorUsedError; + List get primaryColors => throw _privateConstructorUsedError; ThemeMode get themeMode => throw _privateConstructorUsedError; + DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; bool get pureBlack => throw _privateConstructorUsedError; /// Serializes this ThemeProps to a JSON map. @@ -1743,7 +1745,12 @@ abstract class $ThemePropsCopyWith<$Res> { ThemeProps value, $Res Function(ThemeProps) then) = _$ThemePropsCopyWithImpl<$Res, ThemeProps>; @useResult - $Res call({int? primaryColor, ThemeMode themeMode, bool pureBlack}); + $Res call( + {int? primaryColor, + List primaryColors, + ThemeMode themeMode, + DynamicSchemeVariant schemeVariant, + bool pureBlack}); } /// @nodoc @@ -1762,7 +1769,9 @@ class _$ThemePropsCopyWithImpl<$Res, $Val extends ThemeProps> @override $Res call({ Object? primaryColor = freezed, + Object? primaryColors = null, Object? themeMode = null, + Object? schemeVariant = null, Object? pureBlack = null, }) { return _then(_value.copyWith( @@ -1770,10 +1779,18 @@ class _$ThemePropsCopyWithImpl<$Res, $Val extends ThemeProps> ? _value.primaryColor : primaryColor // ignore: cast_nullable_to_non_nullable as int?, + primaryColors: null == primaryColors + ? _value.primaryColors + : primaryColors // ignore: cast_nullable_to_non_nullable + as List, themeMode: null == themeMode ? _value.themeMode : themeMode // ignore: cast_nullable_to_non_nullable as ThemeMode, + schemeVariant: null == schemeVariant + ? _value.schemeVariant + : schemeVariant // ignore: cast_nullable_to_non_nullable + as DynamicSchemeVariant, pureBlack: null == pureBlack ? _value.pureBlack : pureBlack // ignore: cast_nullable_to_non_nullable @@ -1790,7 +1807,12 @@ abstract class _$$ThemePropsImplCopyWith<$Res> __$$ThemePropsImplCopyWithImpl<$Res>; @override @useResult - $Res call({int? primaryColor, ThemeMode themeMode, bool pureBlack}); + $Res call( + {int? primaryColor, + List primaryColors, + ThemeMode themeMode, + DynamicSchemeVariant schemeVariant, + bool pureBlack}); } /// @nodoc @@ -1807,7 +1829,9 @@ class __$$ThemePropsImplCopyWithImpl<$Res> @override $Res call({ Object? primaryColor = freezed, + Object? primaryColors = null, Object? themeMode = null, + Object? schemeVariant = null, Object? pureBlack = null, }) { return _then(_$ThemePropsImpl( @@ -1815,10 +1839,18 @@ class __$$ThemePropsImplCopyWithImpl<$Res> ? _value.primaryColor : primaryColor // ignore: cast_nullable_to_non_nullable as int?, + primaryColors: null == primaryColors + ? _value._primaryColors + : primaryColors // ignore: cast_nullable_to_non_nullable + as List, themeMode: null == themeMode ? _value.themeMode : themeMode // ignore: cast_nullable_to_non_nullable as ThemeMode, + schemeVariant: null == schemeVariant + ? _value.schemeVariant + : schemeVariant // ignore: cast_nullable_to_non_nullable + as DynamicSchemeVariant, pureBlack: null == pureBlack ? _value.pureBlack : pureBlack // ignore: cast_nullable_to_non_nullable @@ -1831,25 +1863,41 @@ class __$$ThemePropsImplCopyWithImpl<$Res> @JsonSerializable() class _$ThemePropsImpl implements _ThemeProps { const _$ThemePropsImpl( - {this.primaryColor, - this.themeMode = ThemeMode.system, - this.pureBlack = false}); + {this.primaryColor = defaultPrimaryColor, + final List primaryColors = defaultPrimaryColors, + this.themeMode = ThemeMode.dark, + this.schemeVariant = DynamicSchemeVariant.tonalSpot, + this.pureBlack = false}) + : _primaryColors = primaryColors; factory _$ThemePropsImpl.fromJson(Map json) => _$$ThemePropsImplFromJson(json); @override + @JsonKey() final int? primaryColor; + final List _primaryColors; + @override + @JsonKey() + List get primaryColors { + if (_primaryColors is EqualUnmodifiableListView) return _primaryColors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_primaryColors); + } + @override @JsonKey() final ThemeMode themeMode; @override @JsonKey() + final DynamicSchemeVariant schemeVariant; + @override + @JsonKey() final bool pureBlack; @override String toString() { - return 'ThemeProps(primaryColor: $primaryColor, themeMode: $themeMode, pureBlack: $pureBlack)'; + return 'ThemeProps(primaryColor: $primaryColor, primaryColors: $primaryColors, themeMode: $themeMode, schemeVariant: $schemeVariant, pureBlack: $pureBlack)'; } @override @@ -1859,16 +1907,25 @@ class _$ThemePropsImpl implements _ThemeProps { other is _$ThemePropsImpl && (identical(other.primaryColor, primaryColor) || other.primaryColor == primaryColor) && + const DeepCollectionEquality() + .equals(other._primaryColors, _primaryColors) && (identical(other.themeMode, themeMode) || other.themeMode == themeMode) && + (identical(other.schemeVariant, schemeVariant) || + other.schemeVariant == schemeVariant) && (identical(other.pureBlack, pureBlack) || other.pureBlack == pureBlack)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, primaryColor, themeMode, pureBlack); + int get hashCode => Object.hash( + runtimeType, + primaryColor, + const DeepCollectionEquality().hash(_primaryColors), + themeMode, + schemeVariant, + pureBlack); /// Create a copy of ThemeProps /// with the given fields replaced by the non-null parameter values. @@ -1889,7 +1946,9 @@ class _$ThemePropsImpl implements _ThemeProps { abstract class _ThemeProps implements ThemeProps { const factory _ThemeProps( {final int? primaryColor, + final List primaryColors, final ThemeMode themeMode, + final DynamicSchemeVariant schemeVariant, final bool pureBlack}) = _$ThemePropsImpl; factory _ThemeProps.fromJson(Map json) = @@ -1898,8 +1957,12 @@ abstract class _ThemeProps implements ThemeProps { @override int? get primaryColor; @override + List get primaryColors; + @override ThemeMode get themeMode; @override + DynamicSchemeVariant get schemeVariant; + @override bool get pureBlack; /// Create a copy of ThemeProps @@ -1925,7 +1988,6 @@ mixin _$Config { DAV? get dav => throw _privateConstructorUsedError; NetworkProps get networkProps => throw _privateConstructorUsedError; VpnProps get vpnProps => throw _privateConstructorUsedError; - @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps get themeProps => throw _privateConstructorUsedError; ProxiesStyle get proxiesStyle => throw _privateConstructorUsedError; WindowProps get windowProps => throw _privateConstructorUsedError; @@ -1955,7 +2017,7 @@ abstract class $ConfigCopyWith<$Res> { DAV? dav, NetworkProps networkProps, VpnProps vpnProps, - @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps themeProps, + ThemeProps themeProps, ProxiesStyle proxiesStyle, WindowProps windowProps, ClashConfig patchClashConfig}); @@ -2152,7 +2214,7 @@ abstract class _$$ConfigImplCopyWith<$Res> implements $ConfigCopyWith<$Res> { DAV? dav, NetworkProps networkProps, VpnProps vpnProps, - @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps themeProps, + ThemeProps themeProps, ProxiesStyle proxiesStyle, WindowProps windowProps, ClashConfig patchClashConfig}); @@ -2267,7 +2329,7 @@ class _$ConfigImpl implements _Config { this.dav, this.networkProps = defaultNetworkProps, this.vpnProps = defaultVpnProps, - @JsonKey(fromJson: ThemeProps.safeFromJson) required this.themeProps, + this.themeProps = defaultThemeProps, this.proxiesStyle = defaultProxiesStyle, this.windowProps = defaultWindowProps, this.patchClashConfig = defaultClashConfig}) @@ -2312,7 +2374,7 @@ class _$ConfigImpl implements _Config { @JsonKey() final VpnProps vpnProps; @override - @JsonKey(fromJson: ThemeProps.safeFromJson) + @JsonKey() final ThemeProps themeProps; @override @JsonKey() @@ -2402,8 +2464,7 @@ abstract class _Config implements Config { final DAV? dav, final NetworkProps networkProps, final VpnProps vpnProps, - @JsonKey(fromJson: ThemeProps.safeFromJson) - required final ThemeProps themeProps, + final ThemeProps themeProps, final ProxiesStyle proxiesStyle, final WindowProps windowProps, final ClashConfig patchClashConfig}) = _$ConfigImpl; @@ -2428,7 +2489,6 @@ abstract class _Config implements Config { @override VpnProps get vpnProps; @override - @JsonKey(fromJson: ThemeProps.safeFromJson) ThemeProps get themeProps; @override ProxiesStyle get proxiesStyle; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 6a12be9..345d6b9 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -17,7 +17,7 @@ _$AppSettingPropsImpl _$$AppSettingPropsImplFromJson( autoLaunch: json['autoLaunch'] as bool? ?? false, silentLaunch: json['silentLaunch'] as bool? ?? false, autoRun: json['autoRun'] as bool? ?? false, - openLogs: json['openLogs'] as bool? ?? false, + openLogs: json['openLogs'] as bool? ?? true, closeConnections: json['closeConnections'] as bool? ?? true, testUrl: json['testUrl'] as String? ?? defaultTestUrl, isAnimateToPage: json['isAnimateToPage'] as bool? ?? true, @@ -221,16 +221,26 @@ const _$ProxyCardTypeEnumMap = { _$ThemePropsImpl _$$ThemePropsImplFromJson(Map json) => _$ThemePropsImpl( - primaryColor: (json['primaryColor'] as num?)?.toInt(), + primaryColor: + (json['primaryColor'] as num?)?.toInt() ?? defaultPrimaryColor, + primaryColors: (json['primaryColors'] as List?) + ?.map((e) => (e as num).toInt()) + .toList() ?? + defaultPrimaryColors, themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? - ThemeMode.system, + ThemeMode.dark, + schemeVariant: $enumDecodeNullable( + _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? + DynamicSchemeVariant.tonalSpot, pureBlack: json['pureBlack'] as bool? ?? false, ); Map _$$ThemePropsImplToJson(_$ThemePropsImpl instance) => { 'primaryColor': instance.primaryColor, + 'primaryColors': instance.primaryColors, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'pureBlack': instance.pureBlack, }; @@ -240,6 +250,18 @@ const _$ThemeModeEnumMap = { ThemeMode.dark: 'dark', }; +const _$DynamicSchemeVariantEnumMap = { + DynamicSchemeVariant.tonalSpot: 'tonalSpot', + DynamicSchemeVariant.fidelity: 'fidelity', + DynamicSchemeVariant.monochrome: 'monochrome', + DynamicSchemeVariant.neutral: 'neutral', + DynamicSchemeVariant.vibrant: 'vibrant', + DynamicSchemeVariant.expressive: 'expressive', + DynamicSchemeVariant.content: 'content', + DynamicSchemeVariant.rainbow: 'rainbow', + DynamicSchemeVariant.fruitSalad: 'fruitSalad', +}; + _$ConfigImpl _$$ConfigImplFromJson(Map json) => _$ConfigImpl( appSetting: json['appSetting'] == null ? defaultAppSettingProps @@ -265,8 +287,9 @@ _$ConfigImpl _$$ConfigImplFromJson(Map json) => _$ConfigImpl( vpnProps: json['vpnProps'] == null ? defaultVpnProps : VpnProps.fromJson(json['vpnProps'] as Map?), - themeProps: - ThemeProps.safeFromJson(json['themeProps'] as Map?), + themeProps: json['themeProps'] == null + ? defaultThemeProps + : ThemeProps.fromJson(json['themeProps'] as Map?), proxiesStyle: json['proxiesStyle'] == null ? defaultProxiesStyle : ProxiesStyle.fromJson( diff --git a/lib/models/generated/core.freezed.dart b/lib/models/generated/core.freezed.dart index d214e99..7762984 100644 --- a/lib/models/generated/core.freezed.dart +++ b/lib/models/generated/core.freezed.dart @@ -1151,6 +1151,178 @@ abstract class _UpdateConfigParams implements UpdateConfigParams { throw _privateConstructorUsedError; } +InitParams _$InitParamsFromJson(Map json) { + return _InitParams.fromJson(json); +} + +/// @nodoc +mixin _$InitParams { + @JsonKey(name: "home-dir") + String get homeDir => throw _privateConstructorUsedError; + int get version => throw _privateConstructorUsedError; + + /// Serializes this InitParams to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of InitParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $InitParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $InitParamsCopyWith<$Res> { + factory $InitParamsCopyWith( + InitParams value, $Res Function(InitParams) then) = + _$InitParamsCopyWithImpl<$Res, InitParams>; + @useResult + $Res call({@JsonKey(name: "home-dir") String homeDir, int version}); +} + +/// @nodoc +class _$InitParamsCopyWithImpl<$Res, $Val extends InitParams> + implements $InitParamsCopyWith<$Res> { + _$InitParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of InitParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? homeDir = null, + Object? version = null, + }) { + return _then(_value.copyWith( + homeDir: null == homeDir + ? _value.homeDir + : homeDir // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$InitParamsImplCopyWith<$Res> + implements $InitParamsCopyWith<$Res> { + factory _$$InitParamsImplCopyWith( + _$InitParamsImpl value, $Res Function(_$InitParamsImpl) then) = + __$$InitParamsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({@JsonKey(name: "home-dir") String homeDir, int version}); +} + +/// @nodoc +class __$$InitParamsImplCopyWithImpl<$Res> + extends _$InitParamsCopyWithImpl<$Res, _$InitParamsImpl> + implements _$$InitParamsImplCopyWith<$Res> { + __$$InitParamsImplCopyWithImpl( + _$InitParamsImpl _value, $Res Function(_$InitParamsImpl) _then) + : super(_value, _then); + + /// Create a copy of InitParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? homeDir = null, + Object? version = null, + }) { + return _then(_$InitParamsImpl( + homeDir: null == homeDir + ? _value.homeDir + : homeDir // ignore: cast_nullable_to_non_nullable + as String, + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$InitParamsImpl implements _InitParams { + const _$InitParamsImpl( + {@JsonKey(name: "home-dir") required this.homeDir, + required this.version}); + + factory _$InitParamsImpl.fromJson(Map json) => + _$$InitParamsImplFromJson(json); + + @override + @JsonKey(name: "home-dir") + final String homeDir; + @override + final int version; + + @override + String toString() { + return 'InitParams(homeDir: $homeDir, version: $version)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$InitParamsImpl && + (identical(other.homeDir, homeDir) || other.homeDir == homeDir) && + (identical(other.version, version) || other.version == version)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, homeDir, version); + + /// Create a copy of InitParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$InitParamsImplCopyWith<_$InitParamsImpl> get copyWith => + __$$InitParamsImplCopyWithImpl<_$InitParamsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$InitParamsImplToJson( + this, + ); + } +} + +abstract class _InitParams implements InitParams { + const factory _InitParams( + {@JsonKey(name: "home-dir") required final String homeDir, + required final int version}) = _$InitParamsImpl; + + factory _InitParams.fromJson(Map json) = + _$InitParamsImpl.fromJson; + + @override + @JsonKey(name: "home-dir") + String get homeDir; + @override + int get version; + + /// Create a copy of InitParams + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$InitParamsImplCopyWith<_$InitParamsImpl> get copyWith => + throw _privateConstructorUsedError; +} + ChangeProxyParams _$ChangeProxyParamsFromJson(Map json) { return _ChangeProxyParams.fromJson(json); } diff --git a/lib/models/generated/core.g.dart b/lib/models/generated/core.g.dart index 97533d6..b951e47 100644 --- a/lib/models/generated/core.g.dart +++ b/lib/models/generated/core.g.dart @@ -100,6 +100,18 @@ Map _$$UpdateConfigParamsImplToJson( 'params': instance.params, }; +_$InitParamsImpl _$$InitParamsImplFromJson(Map json) => + _$InitParamsImpl( + homeDir: json['home-dir'] as String, + version: (json['version'] as num).toInt(), + ); + +Map _$$InitParamsImplToJson(_$InitParamsImpl instance) => + { + 'home-dir': instance.homeDir, + 'version': instance.version, + }; + _$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson( Map json) => _$ChangeProxyParamsImpl( diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 99a9926..1d08eb2 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -173,7 +173,7 @@ extension ProfileExtension on Profile { final disposition = response.headers.value("content-disposition"); final userinfo = response.headers.value('subscription-userinfo'); return await copyWith( - label: label ?? other.getFileNameForDisposition(disposition) ?? id, + label: label ?? utils.getFileNameForDisposition(disposition) ?? id, subscriptionInfo: SubscriptionInfo.formHString(userinfo), ).saveFile(response.data); } diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 384d3b4..2087c0d 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -155,9 +155,9 @@ extension PackageListSelectorStateExt on PackageListSelectorState { (a, b) { return switch (sort) { AccessSortType.none => 0, - AccessSortType.name => other.sortByChar( - other.getPinyin(a.label), - other.getPinyin(b.label), + AccessSortType.name => utils.sortByChar( + utils.getPinyin(a.label), + utils.getPinyin(b.label), ), AccessSortType.time => b.lastUpdateTime.compareTo(a.lastUpdateTime), }; @@ -243,4 +243,4 @@ class ProfileOverrideStateModel with _$ProfileOverrideStateModel { required Set selectedRules, OverrideData? overrideData, }) = _ProfileOverrideStateModel; -} \ No newline at end of file +} diff --git a/lib/plugins/service.dart b/lib/plugins/service.dart index 3b2f3c9..f324601 100644 --- a/lib/plugins/service.dart +++ b/lib/plugins/service.dart @@ -31,7 +31,6 @@ class Service { Future startVpn() async { final options = await clashLib?.getAndroidVpnOptions(); - // commonPrint.log("$options"); return await methodChannel.invokeMethod("startVpn", { 'data': json.encode(options), }); diff --git a/lib/plugins/vpn.dart b/lib/plugins/vpn.dart index 2349805..c2ef56c 100644 --- a/lib/plugins/vpn.dart +++ b/lib/plugins/vpn.dart @@ -8,8 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; abstract mixin class VpnListener { - void onStarted(int fd) {} - void onDnsChanged(String dns) {} } @@ -32,10 +30,6 @@ class Vpn { default: for (final VpnListener listener in _listeners) { switch (call.method) { - case "started": - final fd = call.arguments as int; - listener.onStarted(fd); - break; case "dnsChanged": final dns = call.arguments as String; listener.onDnsChanged(dns); @@ -62,16 +56,6 @@ class Vpn { return await methodChannel.invokeMethod("stop"); } - Future setProtect(int fd) async { - return await methodChannel.invokeMethod("setProtect", {'fd': fd}); - } - - Future resolverProcess(ProcessData process) async { - return await methodChannel.invokeMethod("resolverProcess", { - "data": json.encode(process), - }); - } - void addListener(VpnListener listener) { _listeners.add(listener); } diff --git a/lib/providers/app.dart b/lib/providers/app.dart index 63e7762..6a5ca5f 100644 --- a/lib/providers/app.dart +++ b/lib/providers/app.dart @@ -196,7 +196,7 @@ class ViewSize extends _$ViewSize with AutoDisposeNotifierMixin { ); } - ViewMode get viewMode => other.getViewMode(state.width); + ViewMode get viewMode => utils.getViewMode(state.width); bool get isMobileView => viewMode == ViewMode.mobile; } @@ -208,7 +208,7 @@ double viewWidth(Ref ref) { @riverpod ViewMode viewMode(Ref ref) { - return other.getViewMode(ref.watch(viewWidthProvider)); + return utils.getViewMode(ref.watch(viewWidthProvider)); } @riverpod @@ -356,3 +356,18 @@ class DelayDataSource extends _$DelayDataSource with AutoDisposeNotifierMixin { } } } + +@riverpod +class NeedApply extends _$NeedApply with AutoDisposeNotifierMixin { + @override + bool build() { + return globalState.appState.needApply; + } + + @override + onUpdate(value) { + globalState.appState = globalState.appState.copyWith( + needApply: value, + ); + } +} diff --git a/lib/providers/config.dart b/lib/providers/config.dart index 597cfe3..d0e1005 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -120,7 +120,7 @@ class Profiles extends _$Profiles with AutoDisposeNotifierMixin { (element) => element.label == realLabel && element.id != id) != -1; if (hasDup) { - return _getLabel(other.getOverwriteLabel(realLabel), id); + return _getLabel(utils.getOverwriteLabel(realLabel), id); } else { return label; } diff --git a/lib/providers/generated/app.g.dart b/lib/providers/generated/app.g.dart index 8831640..f4e653c 100644 --- a/lib/providers/generated/app.g.dart +++ b/lib/providers/generated/app.g.dart @@ -22,7 +22,7 @@ final viewWidthProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ViewWidthRef = AutoDisposeProviderRef; -String _$viewModeHash() => r'fbda5aee64803b09b1431b00650ac6e16d044743'; +String _$viewModeHash() => r'736e2acc7e7d98ee30132de1990bf85f9506b47a'; /// See also [viewMode]. @ProviderFor(viewMode) @@ -203,7 +203,7 @@ final runTimeProvider = AutoDisposeNotifierProvider.internal( ); typedef _$RunTime = AutoDisposeNotifier; -String _$viewSizeHash() => r'44a8ff7a1fb1a9ad278b999560bef3ce2c9fea2a'; +String _$viewSizeHash() => r'07f9cce28a69d1496ba4643ef72a739312f6fc28'; /// See also [ViewSize]. @ProviderFor(ViewSize) @@ -336,5 +336,19 @@ final delayDataSourceProvider = ); typedef _$DelayDataSource = AutoDisposeNotifier; +String _$needApplyHash() => r'62ff248d67b0525c6a55e556fbd29a2044e97766'; + +/// See also [NeedApply]. +@ProviderFor(NeedApply) +final needApplyProvider = AutoDisposeNotifierProvider.internal( + NeedApply.new, + name: r'needApplyProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$needApplyHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$NeedApply = 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/generated/config.g.dart b/lib/providers/generated/config.g.dart index b185500..1e92697 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'2023af6ceaf273df480897561565cb3be8054ded'; +String _$profilesHash() => r'a6514c89064e4f42fc31fe7d525088fd26c51016'; /// See also [Profiles]. @ProviderFor(Profiles) diff --git a/lib/providers/generated/state.g.dart b/lib/providers/generated/state.g.dart index 8bca145..7ae3236 100644 --- a/lib/providers/generated/state.g.dart +++ b/lib/providers/generated/state.g.dart @@ -216,7 +216,7 @@ final startButtonSelectorStateProvider = typedef StartButtonSelectorStateRef = AutoDisposeProviderRef; String _$profilesSelectorStateHash() => - r'9fa4447dace0322e888efb38cbee1dabd33e0e71'; + r'aac2deee6e747eceaf62cb5f279ec99ce9227a5a'; /// See also [profilesSelectorState]. @ProviderFor(profilesSelectorState) @@ -1091,7 +1091,7 @@ final currentProfileProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef CurrentProfileRef = AutoDisposeProviderRef; -String _$getProxiesColumnsHash() => r'895705381fe361fa40f16da2f9cb26e8da3293e8'; +String _$getProxiesColumnsHash() => r'725066b5fc21f590a4c2656a1fd5e14ab7079079'; /// See also [getProxiesColumns]. @ProviderFor(getProxiesColumns) @@ -1765,6 +1765,169 @@ class _GetProfileOverrideDataProviderElement String get profileId => (origin as GetProfileOverrideDataProvider).profileId; } +String _$genColorSchemeHash() => r'a27ccae9b5c11d47cd46804f42f8e9dc7946a6c2'; + +/// See also [genColorScheme]. +@ProviderFor(genColorScheme) +const genColorSchemeProvider = GenColorSchemeFamily(); + +/// See also [genColorScheme]. +class GenColorSchemeFamily extends Family { + /// See also [genColorScheme]. + const GenColorSchemeFamily(); + + /// See also [genColorScheme]. + GenColorSchemeProvider call( + Brightness brightness, { + Color? color, + bool isOverride = false, + }) { + return GenColorSchemeProvider( + brightness, + color: color, + isOverride: isOverride, + ); + } + + @override + GenColorSchemeProvider getProviderOverride( + covariant GenColorSchemeProvider provider, + ) { + return call( + provider.brightness, + color: provider.color, + isOverride: provider.isOverride, + ); + } + + 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'genColorSchemeProvider'; +} + +/// See also [genColorScheme]. +class GenColorSchemeProvider extends AutoDisposeProvider { + /// See also [genColorScheme]. + GenColorSchemeProvider( + Brightness brightness, { + Color? color, + bool isOverride = false, + }) : this._internal( + (ref) => genColorScheme( + ref as GenColorSchemeRef, + brightness, + color: color, + isOverride: isOverride, + ), + from: genColorSchemeProvider, + name: r'genColorSchemeProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$genColorSchemeHash, + dependencies: GenColorSchemeFamily._dependencies, + allTransitiveDependencies: + GenColorSchemeFamily._allTransitiveDependencies, + brightness: brightness, + color: color, + isOverride: isOverride, + ); + + GenColorSchemeProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.brightness, + required this.color, + required this.isOverride, + }) : super.internal(); + + final Brightness brightness; + final Color? color; + final bool isOverride; + + @override + Override overrideWith( + ColorScheme Function(GenColorSchemeRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: GenColorSchemeProvider._internal( + (ref) => create(ref as GenColorSchemeRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + brightness: brightness, + color: color, + isOverride: isOverride, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _GenColorSchemeProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is GenColorSchemeProvider && + other.brightness == brightness && + other.color == color && + other.isOverride == isOverride; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, brightness.hashCode); + hash = _SystemHash.combine(hash, color.hashCode); + hash = _SystemHash.combine(hash, isOverride.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin GenColorSchemeRef on AutoDisposeProviderRef { + /// The parameter `brightness` of this provider. + Brightness get brightness; + + /// The parameter `color` of this provider. + Color? get color; + + /// The parameter `isOverride` of this provider. + bool get isOverride; +} + +class _GenColorSchemeProviderElement + extends AutoDisposeProviderElement with GenColorSchemeRef { + _GenColorSchemeProviderElement(super.provider); + + @override + Brightness get brightness => (origin as GenColorSchemeProvider).brightness; + @override + Color? get color => (origin as GenColorSchemeProvider).color; + @override + bool get isOverride => (origin as GenColorSchemeProvider).isOverride; +} + String _$profileOverrideStateHash() => r'16d7c75849ed077d60553e5d2bba4ed54b307971'; diff --git a/lib/providers/state.dart b/lib/providers/state.dart index 42b5ff1..26cb7ff 100644 --- a/lib/providers/state.dart +++ b/lib/providers/state.dart @@ -1,6 +1,7 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -205,7 +206,7 @@ ProfilesSelectorState profilesSelectorState(Ref ref) { final currentProfileId = ref.watch(currentProfileIdProvider); final profiles = ref.watch(profilesProvider); final columns = ref.watch( - viewWidthProvider.select((state) => other.getProfilesColumns(state))); + viewWidthProvider.select((state) => utils.getProfilesColumns(state))); return ProfilesSelectorState( profiles: profiles, currentProfileId: currentProfileId, @@ -406,7 +407,7 @@ int getProxiesColumns(Ref ref) { final viewWidth = ref.watch(viewWidthProvider); final proxiesLayout = ref.watch(proxiesStyleSettingProvider.select((state) => state.layout)); - return other.getProxiesColumns(viewWidth, proxiesLayout); + return utils.getProxiesColumns(viewWidth, proxiesLayout); } ProxyCardState _getProxyCardState( @@ -504,3 +505,32 @@ OverrideData? getProfileOverrideData(Ref ref, String profileId) { ), ); } + +@riverpod +ColorScheme genColorScheme( + Ref ref, + Brightness brightness, { + Color? color, + bool isOverride = false, +}) { + final vm2 = ref.watch( + themeSettingProvider.select( + (state) => VM2( + a: state.primaryColor, + b: state.schemeVariant, + ), + ), + ); + if (color == null && (isOverride == true || vm2.a == null)) { + final colorSchemes = ref.watch(appSchemesProvider); + return colorSchemes.getColorSchemeForBrightness( + brightness, + vm2.b, + ); + } + return ColorScheme.fromSeed( + seedColor: color ?? Color(vm2.a!), + brightness: brightness, + dynamicSchemeVariant: vm2.b, + ); +} diff --git a/lib/state.dart b/lib/state.dart index 985d309..929536f 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -20,6 +20,7 @@ typedef UpdateTasks = List; class GlobalState { static GlobalState? _instance; + Map cacheScrollPosition = {}; bool isService = false; Timer? timer; Timer? groupsUpdateTimer; @@ -65,7 +66,7 @@ class GlobalState { ); await globalState.migrateOldData(config); await AppLocalizations.load( - other.getLocaleForString(config.appSetting.locale) ?? + utils.getLocaleForString(config.appSetting.locale) ?? WidgetsBinding.instance.platformDispatcher.locale, ); } @@ -110,7 +111,6 @@ class GlobalState { Future handleStop() async { startTime = null; await clashCore.stopListener(); - await clashLib?.stopTun(); await service?.stopVpn(); stopUpdateTasks(); } diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index d68c93b..18c3a4c 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -32,7 +32,7 @@ class InfoHeader extends StatelessWidget { return Padding( padding: padding ?? baseInfoEdgeInsets, child: Row( - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( diff --git a/lib/widgets/color_scheme_box.dart b/lib/widgets/color_scheme_box.dart index ce6c17f..f9af319 100644 --- a/lib/widgets/color_scheme_box.dart +++ b/lib/widgets/color_scheme_box.dart @@ -1,6 +1,6 @@ -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; +import 'package:fl_clash/providers/providers.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'card.dart'; import 'grid.dart'; @@ -11,33 +11,17 @@ class ColorSchemeBox extends StatelessWidget { const ColorSchemeBox({ super.key, - this.primaryColor, + required this.primaryColor, this.onPressed, this.isSelected, }); - ThemeData _getTheme(BuildContext context) { - if (primaryColor != null) { - return Theme.of(context).copyWith( - colorScheme: ColorScheme.fromSeed( - seedColor: primaryColor!, - brightness: Theme.of(context).brightness, - ), - ); - } else { - return Theme.of(context).copyWith( - colorScheme: globalState.appState.colorSchemes - .getColorSchemeForBrightness(Theme.of(context).brightness), - ); - } - } - @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: 1, - child: Theme( - data: _getTheme(context), + child: PrimaryColorBox( + primaryColor: primaryColor, child: Builder( builder: (context) { final colorScheme = Theme.of(context).colorScheme; @@ -101,3 +85,32 @@ class ColorSchemeBox extends StatelessWidget { ); } } + +class PrimaryColorBox extends ConsumerWidget { + final Color? primaryColor; + final Widget child; + + const PrimaryColorBox({ + super.key, + required this.primaryColor, + required this.child, + }); + + @override + Widget build(BuildContext context, ref) { + final themeData = Theme.of(context); + final colorScheme = ref.watch( + genColorSchemeProvider( + themeData.brightness, + color: primaryColor, + isOverride: true, + ), + ); + return Theme( + data: themeData.copyWith( + colorScheme: colorScheme, + ), + child: child, + ); + } +} diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 353bb0b..f02b558 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -278,6 +278,9 @@ class ListItem extends StatelessWidget { onBack: action, title: openDelegate.title, body: child, + actions: [ + if (openDelegate.action != null) openDelegate.action!, + ], ); }, ); @@ -494,133 +497,4 @@ Widget generateListView(List items) { bottom: 16, ), ); -} - -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(); - } -} +} \ No newline at end of file diff --git a/lib/widgets/palette.dart b/lib/widgets/palette.dart new file mode 100644 index 0000000..8086868 --- /dev/null +++ b/lib/widgets/palette.dart @@ -0,0 +1,442 @@ +import 'dart:math' as math; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +@immutable +class Palette extends StatefulWidget { + const Palette({ + super.key, + required this.controller, + }); + + final ValueNotifier controller; + + @override + State createState() => _PaletteState(); +} + +class _PaletteState extends State { + final double _thickness = 20; + final double _radius = 4; + final double _padding = 8; + final GlobalKey renderBoxKey = GlobalKey(); + bool isSquare = false; + bool isTrack = false; + late double colorHue; + late double colorSaturation; + late double colorValue; + + late FocusNode _focusNode; + + Color get value => widget.controller.value; + + HSVColor get color => HSVColor.fromColor(value); + + @override + void initState() { + super.initState(); + colorHue = color.hue; + colorSaturation = color.saturation; + colorValue = color.value; + _focusNode = FocusNode(); + } + + _handleChange() { + widget.controller.value = HSVColor.fromAHSV( + color.alpha, + colorHue, + colorSaturation, + colorValue, + ).toColor(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + double trackRadius(Size size) => + math.min(size.width, size.height) / 2 - _thickness; + + static double squareRadius(double radius, double trackSquarePadding) => + (radius - trackSquarePadding) / math.sqrt(2); + + void onStart(Offset offset) { + final RenderBox renderBox = + renderBoxKey.currentContext!.findRenderObject()! as RenderBox; + final size = renderBox.size; + final radius = trackRadius(size); + final radiusOuter = radius + _thickness; + final effectiveSquareRadius = squareRadius(radius, _padding); + final startPosition = renderBox.localToGlobal(Offset.zero); + final center = Offset(size.width / 2, size.height / 2); + final vector = offset - startPosition - center; + final vectorLength = _Computer.vectorLength(vector); + isSquare = vector.dx.abs() < effectiveSquareRadius && + vector.dy.abs() < effectiveSquareRadius; + isTrack = vectorLength >= radius && vectorLength <= radiusOuter; + if (isSquare) { + colorSaturation = + _Computer.vectorToSaturation(vector.dx, effectiveSquareRadius) + .clamp(0.0, 1.0); + colorValue = _Computer.vectorToValue(vector.dy, effectiveSquareRadius) + .clamp(0.0, 1.0); + _handleChange(); + } else if (isTrack) { + colorHue = _Computer.vectorToHue(vector); + _handleChange(); + } else { + isTrack = false; + isSquare = false; + } + } + + void onUpdate(Offset offset) { + final RenderBox renderBox = + renderBoxKey.currentContext!.findRenderObject()! as RenderBox; + final size = renderBox.size; + final radius = trackRadius(size); + final effectiveSquareRadius = squareRadius(radius, _padding); + final startPosition = renderBox.localToGlobal(Offset.zero); + final center = Offset(size.width / 2, size.height / 2); + final vector = offset - startPosition - center; + if (isSquare) { + isTrack = false; + colorSaturation = + _Computer.vectorToSaturation(vector.dx, effectiveSquareRadius) + .clamp(0.0, 1.0); + colorValue = _Computer.vectorToValue(vector.dy, effectiveSquareRadius) + .clamp(0.0, 1.0); + + _handleChange(); + } else if (isTrack) { + isSquare = false; + colorHue = _Computer.vectorToHue(vector); + _handleChange(); + } else { + isTrack = false; + isSquare = false; + } + } + + void onEnd() { + isTrack = false; + isSquare = false; + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: widget.controller, + builder: (_, __, ___) { + return GestureDetector( + dragStartBehavior: DragStartBehavior.down, + onVerticalDragDown: (DragDownDetails details) => + onStart(details.globalPosition), + onVerticalDragUpdate: (DragUpdateDetails details) => + onUpdate(details.globalPosition), + onHorizontalDragUpdate: (DragUpdateDetails details) => + onUpdate(details.globalPosition), + onVerticalDragEnd: (DragEndDetails details) => onEnd(), + onHorizontalDragEnd: (DragEndDetails details) => onEnd(), + onTapUp: (TapUpDetails details) => onEnd(), + child: SizedBox( + key: renderBoxKey, + child: Focus( + focusNode: _focusNode, + child: MouseRegion( + cursor: WidgetStateMouseCursor.clickable, + child: Stack( + fit: StackFit.expand, + children: [ + RepaintBoundary( + child: CustomPaint( + painter: _ShadePainter( + colorHue: colorHue, + colorSaturation: colorSaturation, + colorValue: colorValue, + thickness: _thickness, + padding: _padding, + trackBorderRadius: _radius, + ), + ), + ), + CustomPaint( + painter: _ShadeThumbPainter( + colorSaturation: colorSaturation, + colorValue: colorValue, + thickness: _thickness, + padding: _padding, + ), + ), + RepaintBoundary( + child: CustomPaint( + painter: _TrackPainter( + thickness: _thickness, + ticks: 360, + ), + ), + ), + CustomPaint( + painter: _TrackThumbPainter( + colorHue: colorHue, + thickness: _thickness, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} + +class _ShadePainter extends CustomPainter { + const _ShadePainter({ + required this.colorHue, + required this.colorSaturation, + required this.colorValue, + required this.thickness, + required this.padding, + required this.trackBorderRadius, + }) : super(); + + final double colorHue; + final double colorSaturation; + final double colorValue; + + final double thickness; + final double padding; + final double trackBorderRadius; + + static double trackRadius(Size size, double trackWidth) => + math.min(size.width, size.height) / 2 - trackWidth / 2; + + static double squareRadius( + double radius, double trackWidth, double padding) => + (radius - trackWidth / 2 - padding) / math.sqrt(2); + + @override + void paint(Canvas canvas, Size size) { + final Offset center = Offset(size.width / 2, size.height / 2); + final double radius = trackRadius(size, thickness); + final double effectiveSquareRadius = squareRadius( + radius, + thickness, + padding, + ); + + final Rect rectBox = Rect.fromLTWH( + center.dx - effectiveSquareRadius, + center.dy - effectiveSquareRadius, + effectiveSquareRadius * 2, + effectiveSquareRadius * 2); + final RRect rRect = RRect.fromRectAndRadius( + rectBox, + Radius.circular(trackBorderRadius), + ); + + final Shader horizontal = LinearGradient( + colors: [ + Colors.white, + HSVColor.fromAHSV(1, colorHue, 1, 1).toColor() + ], + ).createShader(rectBox); + canvas.drawRRect( + rRect, + Paint() + ..style = PaintingStyle.fill + ..shader = horizontal); + + final Shader vertical = const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black], + ).createShader(rectBox); + canvas.drawRRect( + rRect, + Paint() + ..style = PaintingStyle.fill + ..shader = vertical); + } + + @override + bool shouldRepaint(_ShadePainter oldDelegate) { + return oldDelegate.thickness != thickness || + oldDelegate.padding != padding || + oldDelegate.trackBorderRadius != trackBorderRadius || + oldDelegate.colorHue != colorHue || + oldDelegate.colorSaturation != colorSaturation || + oldDelegate.colorValue != colorValue; + } +} + +class _TrackPainter extends CustomPainter { + const _TrackPainter({ + this.ticks = 360, + required this.thickness, + }) : super(); + final int ticks; + final double thickness; + + @override + void paint(Canvas canvas, Size size) { + final Offset center = Offset(size.width / 2, size.height / 2); + + const double rads = (2 * math.pi) / 360; + const double step = 1; + const double aliasing = 0.5; + + final double shortestRectSide = math.min(size.width, size.height); + + final Rect rectCircle = Rect.fromCenter( + center: center, + width: shortestRectSide - thickness, + height: shortestRectSide - thickness, + ); + + for (int i = 0; i < ticks; i++) { + final double sRad = (i - aliasing) * rads; + final double eRad = (i + step) * rads; + final Paint segmentPaint = Paint() + ..color = HSVColor.fromAHSV(1, i.toDouble(), 1, 1).toColor() + ..style = PaintingStyle.stroke + ..strokeWidth = thickness; + canvas.drawArc( + rectCircle, + sRad, + sRad - eRad, + false, + segmentPaint, + ); + } + } + + @override + bool shouldRepaint(_TrackPainter oldDelegate) { + return oldDelegate.thickness != thickness || oldDelegate.ticks != ticks; + } +} + +class _ShadeThumbPainter extends CustomPainter { + const _ShadeThumbPainter({ + required this.colorSaturation, + required this.colorValue, + required this.thickness, + required this.padding, + }) : super(); + + final double colorSaturation; + final double colorValue; + final double thickness; + final double padding; + + static double trackRadius(Size size, double thickness) => + math.min(size.width, size.height) / 2 - thickness / 2; + + static double squareRadius( + double radius, double thickness, double trackSquarePadding) => + (radius - thickness / 2 - trackSquarePadding) / math.sqrt(2); + + @override + void paint(Canvas canvas, Size size) { + final Offset center = Offset(size.width / 2, size.height / 2); + final double radius = trackRadius(size, thickness); + final double effectiveSquareRadius = + squareRadius(radius, thickness, padding); + + final Paint paintBlack = Paint() + ..color = Colors.black + ..strokeWidth = 5 + ..style = PaintingStyle.stroke; + final Paint paintWhite = Paint() + ..color = Colors.white + ..strokeWidth = 3 + ..style = PaintingStyle.stroke; + + final double paletteX = _Computer.saturationToVector( + colorSaturation, effectiveSquareRadius, center.dx); + final double paletteY = + _Computer.valueToVector(colorValue, effectiveSquareRadius, center.dy); + final Offset paletteVector = Offset(paletteX, paletteY); + canvas.drawCircle(paletteVector, 12, paintBlack); + canvas.drawCircle(paletteVector, 12, paintWhite); + } + + @override + bool shouldRepaint(_ShadeThumbPainter oldDelegate) { + return oldDelegate.thickness != thickness || + oldDelegate.colorSaturation != colorSaturation || + oldDelegate.colorValue != colorValue || + oldDelegate.padding != padding; + } +} + +class _TrackThumbPainter extends CustomPainter { + const _TrackThumbPainter({ + required this.colorHue, + required this.thickness, + }) : super(); + + final double colorHue; + final double thickness; + + static double trackRadius(Size size, double thickness) => + math.min(size.width, size.height) / 2 - thickness / 2; + + @override + void paint(Canvas canvas, Size size) { + final Offset center = Offset(size.width / 2, size.height / 2); + final double radius = trackRadius(size, thickness); + final Paint paintBlack = Paint() + ..color = Colors.black + ..strokeWidth = 5 + ..style = PaintingStyle.stroke; + final Paint paintWhite = Paint() + ..color = Colors.white + ..strokeWidth = 3 + ..style = PaintingStyle.stroke; + final Offset track = _Computer.hueToVector( + (colorHue + 360.0) * math.pi / 180.0, + radius, + center, + ); + canvas.drawCircle(track, thickness / 2 + 4, paintBlack); + canvas.drawCircle(track, thickness / 2 + 4, paintWhite); + } + + @override + bool shouldRepaint(_TrackThumbPainter oldDelegate) { + return oldDelegate.thickness != thickness || + oldDelegate.colorHue != colorHue; + } +} + +class _Computer { + static double vectorLength(Offset vector) => + math.sqrt(vector.dx * vector.dx + vector.dy * vector.dy); + + static double vectorToHue(Offset vector) => + (((math.atan2(vector.dy, vector.dx)) * 180.0 / math.pi) + 360.0) % 360.0; + + static double vectorToSaturation(double vectorX, double squareRadius) => + vectorX * 0.5 / squareRadius + 0.5; + + static double vectorToValue(double vectorY, double squareRadius) => + 0.5 - vectorY * 0.5 / squareRadius; + + static Offset hueToVector(double h, double radius, Offset center) => Offset( + math.cos(h) * radius + center.dx, math.sin(h) * radius + center.dy); + + static double saturationToVector( + double s, double squareRadius, double centerX) => + (s - 0.5) * squareRadius / 0.5 + centerX; + + static double valueToVector(double l, double squareRadius, double centerY) => + (0.5 - l) * squareRadius / 0.5 + centerY; +} diff --git a/lib/widgets/popup.dart b/lib/widgets/popup.dart index 92e919b..c5f593c 100644 --- a/lib/widgets/popup.dart +++ b/lib/widgets/popup.dart @@ -126,7 +126,7 @@ class _CommonPopupBoxState extends State { Navigator.of(context) .push( CommonPopupRoute( - barrierLabel: other.id, + barrierLabel: utils.id, builder: (BuildContext context) { return widget.popup; }, diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 55d3440..1451cc8 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -42,11 +42,13 @@ class CommonScaffold extends StatefulWidget { required Widget body, required String title, required Function onBack, + required List actions, }) : this( key: key, body: body, title: title, automaticallyImplyLeading: false, + actions: actions, leading: IconButton( icon: const BackButtonIcon(), onPressed: () { diff --git a/lib/widgets/scroll.dart b/lib/widgets/scroll.dart index ea9a7e4..461afd6 100644 --- a/lib/widgets/scroll.dart +++ b/lib/widgets/scroll.dart @@ -1,3 +1,7 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/common/list.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; class CommonScrollBar extends StatelessWidget { @@ -45,3 +49,197 @@ class CommonAutoHiddenScrollBar extends StatelessWidget { ); } } + +class ScrollToEndBox extends StatefulWidget { + final ScrollController controller; + final List dataSource; + final Widget child; + final Key cacheKey; + + const ScrollToEndBox({ + super.key, + required this.child, + required this.controller, + required this.cacheKey, + required this.dataSource, + }); + + @override + State> createState() => _ScrollToEndBoxState(); +} + +class _ScrollToEndBoxState extends State> { + final equals = ListEquality(); + + _handleTryToEnd() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final double offset = + globalState.cacheScrollPosition[widget.cacheKey] ?? -1; + if (offset < 0) { + widget.controller.animateTo( + duration: kThemeAnimationDuration, + widget.controller.position.maxScrollExtent, + curve: Curves.easeOut, + ); + } + }); + } + + @override + void initState() { + super.initState(); + globalState.cacheScrollPosition[widget.cacheKey] = -1; + } + + @override + void didUpdateWidget(ScrollToEndBox oldWidget) { + super.didUpdateWidget(oldWidget); + if (!equals.equals(oldWidget.dataSource, widget.dataSource)) { + _handleTryToEnd(); + } + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (details) { + double offset = + details.metrics.pixels == details.metrics.maxScrollExtent + ? -1 + : details.metrics.pixels; + globalState.cacheScrollPosition[widget.cacheKey] = offset; + return false; + }, + child: widget.child, + ); + } +} + +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/widgets.dart b/lib/widgets/widgets.dart index 22ccf6c..3c862a5 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -29,3 +29,4 @@ export 'wave.dart'; export 'scroll.dart'; export 'dialog.dart'; export 'effect.dart'; +export 'palette.dart'; diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 0a74486..5dcff25 100755 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -744,7 +744,7 @@ "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash.debug; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/plugins/flutter_distributor b/plugins/flutter_distributor index 44a8396..c5c06ee 160000 --- a/plugins/flutter_distributor +++ b/plugins/flutter_distributor @@ -1 +1 @@ -Subproject commit 44a8396d30bbc21348f8e499f4340765dbcb564b +Subproject commit c5c06ee67d8eeee94c0cf2429fe9fe5bb4180bc2 diff --git a/pubspec.yaml b/pubspec.yaml index eb9c22c..b410656 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.81+202504081 +version: 0.8.82+202504182 environment: sdk: '>=3.1.0 <4.0.0' diff --git a/release_telegram.py b/release_telegram.py index 4e4c253..af4f159 100644 --- a/release_telegram.py +++ b/release_telegram.py @@ -19,16 +19,26 @@ media = [] files = {} i = 1 + +releaseKeywords = [ + "windows-amd64-setup", + "android-arm64", + "macos-arm64", + "macos-amd64" +] + for file in os.listdir(DIST_DIR): file_path = os.path.join(DIST_DIR, file) if os.path.isfile(file_path): - file_key = f"file{i}" - media.append({ - "type": "document", - "media": f"attach://{file_key}" - }) - files[file_key] = open(file_path, 'rb') - i += 1 + file_lower = file.lower() + if any(kw in file_lower for kw in releaseKeywords): + file_key = f"file{i}" + media.append({ + "type": "document", + "media": f"attach://{file_key}" + }) + files[file_key] = open(file_path, 'rb') + i += 1 if TAG: text += f"\n**{TAG}**\n" @@ -56,4 +66,4 @@ response = requests.post( files=files ) -print("Response JSON:", response.json()) \ No newline at end of file +print("Response JSON:", response.json()) diff --git a/setup.dart b/setup.dart index 8c5f305..2df19f8 100755 --- a/setup.dart +++ b/setup.dart @@ -437,7 +437,7 @@ class BuildCommand extends Command { await Build.exec( name: name, Build.getExecutable( - "flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose $args --build-dart-define=APP_ENV=$env", + "flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose$args --build-dart-define=APP_ENV=$env", ), ); } @@ -485,7 +485,7 @@ class BuildCommand extends Command { _buildDistributor( target: target, targets: "exe,zip", - args: "--description $archName", + args: " --description $archName", env: env, ); return; @@ -507,7 +507,7 @@ class BuildCommand extends Command { target: target, targets: targets, args: - "--description $archName --build-target-platform $defaultTarget", + " --description $archName --build-target-platform $defaultTarget", env: env, ); return; @@ -526,7 +526,7 @@ class BuildCommand extends Command { target: target, targets: "apk", args: - "--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}", + ",split-per-abi --build-target-platform ${defaultTargets.join(",")}", env: env, ); return; @@ -535,7 +535,7 @@ class BuildCommand extends Command { _buildDistributor( target: target, targets: "dmg", - args: "--description $archName", + args: " --description $archName", env: env, ); return;