From ed7ade0d729d03638a63fa6ed3ad60de7de9c3ab Mon Sep 17 00:00:00 2001 From: chen08209 Date: Fri, 26 Jul 2024 08:05:22 +0800 Subject: [PATCH] Add proxy-only traffic statistics Update core Optimize more details --- android/app/src/main/AndroidManifest.xml | 29 +- .../kotlin/com/follow/clash/FilesProvider.kt | 112 ++++++ .../kotlin/com/follow/clash/GlobalState.kt | 2 - .../com/follow/clash/plugins/AppPlugin.kt | 51 ++- android/app/src/main/res/xml/file_paths.xml | 6 + core/Clash.Meta | 2 +- core/common.go | 3 +- core/go.mod | 18 +- core/go.sum | 36 +- core/hub.go | 65 ++-- core/{status.go => state.go} | 22 +- lib/application.dart | 4 +- lib/clash/core.dart | 29 +- lib/clash/generated/clash_ffi.dart | 25 +- lib/common/constant.dart | 2 +- lib/common/request.dart | 2 +- lib/controller.dart | 60 ++-- lib/fragments/access.dart | 321 +++++++++--------- lib/fragments/config.dart | 33 +- lib/fragments/connections.dart | 24 +- .../dashboard/network_detection.dart | 36 +- lib/fragments/profiles/edit_profile.dart | 146 +++++++- lib/fragments/profiles/profiles.dart | 274 +++++---------- lib/fragments/profiles/view_profile.dart | 207 ----------- lib/fragments/proxies/card.dart | 211 +++++++----- lib/fragments/proxies/common.dart | 16 +- lib/fragments/proxies/list.dart | 45 ++- lib/fragments/proxies/tab.dart | 89 +++-- lib/fragments/resources.dart | 33 +- lib/l10n/arb/intl_en.arb | 5 +- lib/l10n/arb/intl_zh_CN.arb | 5 +- lib/l10n/intl/messages_en.dart | 6 + lib/l10n/intl/messages_zh_CN.dart | 4 + lib/l10n/l10n.dart | 30 ++ lib/models/app.dart | 21 +- lib/models/config.dart | 27 +- lib/models/file.dart | 21 ++ lib/models/generated/config.freezed.dart | 131 ++++--- lib/models/generated/config.g.dart | 11 +- lib/models/generated/file.freezed.dart | 150 ++++++++ lib/models/generated/proxy.freezed.dart | 38 ++- lib/models/generated/proxy.g.dart | 2 + lib/models/generated/selector.freezed.dart | 280 +++++++++++---- lib/models/models.dart | 3 +- lib/models/proxy.dart | 12 + lib/models/selector.dart | 13 +- lib/pages/home.dart | 15 +- lib/plugins/app.dart | 9 +- lib/plugins/proxy.dart | 5 +- lib/state.dart | 36 +- lib/widgets/android_container.dart | 1 - lib/widgets/builder.dart | 40 +++ lib/widgets/card.dart | 40 +-- ...ge_container.dart => clash_container.dart} | 39 ++- lib/widgets/list.dart | 20 +- lib/widgets/scaffold.dart | 8 +- lib/widgets/sheet.dart | 12 +- lib/widgets/side_sheet.dart | 40 +-- lib/widgets/widgets.dart | 5 +- pubspec.lock | 32 -- pubspec.yaml | 2 +- 61 files changed, 1764 insertions(+), 1202 deletions(-) create mode 100644 android/app/src/main/kotlin/com/follow/clash/FilesProvider.kt create mode 100644 android/app/src/main/res/xml/file_paths.xml rename core/{status.go => state.go} (68%) delete mode 100644 lib/fragments/profiles/view_profile.dart create mode 100644 lib/models/file.dart create mode 100644 lib/models/generated/file.freezed.dart create mode 100644 lib/widgets/builder.dart rename lib/widgets/{clash_message_container.dart => clash_container.dart} (62%) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ca5d76c..d2383e2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -78,7 +78,8 @@ android:icon="@drawable/ic_stat_name" android:foregroundServiceType="specialUse" android:label="FlClash" - android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + > @@ -86,11 +87,35 @@ android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" /> + + + + + + + + + + + + android:permission="android.permission.BIND_VPN_SERVICE" + > diff --git a/android/app/src/main/kotlin/com/follow/clash/FilesProvider.kt b/android/app/src/main/kotlin/com/follow/clash/FilesProvider.kt new file mode 100644 index 0000000..5ae2f06 --- /dev/null +++ b/android/app/src/main/kotlin/com/follow/clash/FilesProvider.kt @@ -0,0 +1,112 @@ +package com.follow.clash + +import android.database.Cursor +import android.database.MatrixCursor +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract.Document +import android.provider.DocumentsContract.Root +import android.provider.DocumentsProvider +import java.io.File +import java.io.FileNotFoundException + + +class FilesProvider : DocumentsProvider() { + + companion object { + private const val DEFAULT_ROOT_ID = "0" + + private val DEFAULT_DOCUMENT_COLUMNS = arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_FLAGS, + Document.COLUMN_SIZE, + ) + private val DEFAULT_ROOT_COLUMNS = arrayOf( + Root.COLUMN_ROOT_ID, + Root.COLUMN_FLAGS, + Root.COLUMN_ICON, + Root.COLUMN_TITLE, + Root.COLUMN_SUMMARY, + Root.COLUMN_DOCUMENT_ID + ) + } + + override fun onCreate(): Boolean { + return true + } + + override fun queryRoots(projection: Array?): Cursor { + return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply { + newRow().apply { + add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID) + add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY) + add(Root.COLUMN_ICON, R.mipmap.ic_launcher) + add(Root.COLUMN_TITLE, context!!.getString(R.string.fl_clash)) + add(Root.COLUMN_SUMMARY, "Data") + add(Root.COLUMN_DOCUMENT_ID, "/") + } + } + } + + + override fun queryChildDocuments( + parentDocumentId: String, + projection: Array?, + sortOrder: String? + ): Cursor { + val result = MatrixCursor(resolveDocumentProjection(projection)) + val parentFile = if (parentDocumentId == "/") { + context?.filesDir + } else { + File(parentDocumentId) + } ?: throw FileNotFoundException("Parent directory not found") + parentFile.listFiles()?.forEach { file -> + includeFile(result, file) + } + return result + } + + override fun queryDocument(documentId: String, projection: Array?): Cursor { + val result = MatrixCursor(resolveDocumentProjection(projection)) + val file = File(documentId) + includeFile(result, file) + return result + } + + override fun openDocument( + documentId: String, + mode: String, + signal: CancellationSignal? + ): ParcelFileDescriptor { + val file = File(documentId) + val accessMode = ParcelFileDescriptor.parseMode(mode) + return ParcelFileDescriptor.open(file, accessMode) + } + + private fun includeFile(result: MatrixCursor, file: File) { + result.newRow().apply { + add(Document.COLUMN_DOCUMENT_ID, file.absolutePath) + add(Document.COLUMN_DISPLAY_NAME, file.name) + add(Document.COLUMN_SIZE, file.length()) + add( + Document.COLUMN_FLAGS, + Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE + ) + add(Document.COLUMN_MIME_TYPE, getDocumentType(file)) + } + } + + private fun getDocumentType(file: File): String { + return if (file.isDirectory) { + Document.MIME_TYPE_DIR + } else { + "application/octet-stream" + } + } + + private fun resolveDocumentProjection(projection: Array?): Array { + return projection ?: DEFAULT_DOCUMENT_COLUMNS + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt index 9da48ce..237e3df 100644 --- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -57,8 +57,6 @@ object GlobalState { serviceEngine?.dartExecutor?.executeDartEntrypoint( vpnService, ) - - Log.e("FlClashVpnService", "initServiceEngine ===>") } } } diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt index ef81b23..809019d 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt @@ -4,12 +4,14 @@ import android.Manifest import android.app.Activity import android.app.ActivityManager import android.content.Context +import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.net.ConnectivityManager import android.os.Build import android.widget.Toast import androidx.core.content.ContextCompat.getSystemService +import androidx.core.content.FileProvider import androidx.core.content.getSystemService import com.follow.clash.GlobalState import com.follow.clash.extensions.getBase64 @@ -28,6 +30,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File import java.net.InetSocketAddress @@ -61,7 +64,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } private fun tip(message: String?) { - if(GlobalState.flutterEngine == null){ + if (GlobalState.flutterEngine == null) { if (toast != null) { toast!!.cancel() } @@ -164,12 +167,56 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware result.success(true) } + "openFile" -> { + val path = call.argument("path")!! + openFile(path) + result.success(true) + } + else -> { result.notImplemented(); } } } + private fun openFile(path: String) { + context?.let { + val file = File(path) + val uri = FileProvider.getUriForFile( + it, + "${it.packageName}.fileProvider", + file + ) + + val intent = Intent(Intent.ACTION_VIEW).setDataAndType( + uri, + "text/plain" + ) + + val flags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + + val resInfoList = it.packageManager.queryIntentActivities( + intent, PackageManager.MATCH_DEFAULT_ONLY + ) + + for (resolveInfo in resInfoList) { + val packageName = resolveInfo.activityInfo.packageName + it.grantUriPermission( + packageName, + uri, + flags + ) + } + + try { + activity?.startActivity(intent) + } catch (e: Exception) { + println(e) + } + } + } + private fun updateExcludeFromRecents(value: Boolean?) { if (context == null) return val am = getSystemService(context!!, ActivityManager::class.java) @@ -241,4 +288,4 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware channel.invokeMethod("exit", null) activity = null } -} +} \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..798f296 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/core/Clash.Meta b/core/Clash.Meta index 0292a65..689946f 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 0292a65f16e57eafdd8995489e12e84c955a8076 +Subproject commit 689946f7a68b2e20ced1b3336f8dba43c3e2afb6 diff --git a/core/common.go b/core/common.go index 53ea93f..cb4e9ee 100644 --- a/core/common.go +++ b/core/common.go @@ -410,7 +410,7 @@ func patchSelectGroup() { var applyLock sync.Mutex -func applyConfig() { +func applyConfig() error { applyLock.Lock() defer applyLock.Unlock() cfg, err := config.ParseRawConfig(currentConfig) @@ -428,4 +428,5 @@ func applyConfig() { hub.UltraApplyConfig(cfg, true) patchSelectGroup() } + return err } diff --git a/core/go.mod b/core/go.mod index 9fa0de2..5d3a967 100644 --- a/core/go.mod +++ b/core/go.mod @@ -16,7 +16,6 @@ require ( github.com/3andne/restls-client-go v0.1.6 // indirect github.com/RyuaNerin/go-krypto v1.2.4 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -46,22 +45,23 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect + github.com/metacubex/chacha v0.1.0 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect - github.com/metacubex/sing-shadowsocks v0.2.6 // indirect - github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect - github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e // indirect - github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // indirect + github.com/metacubex/sing-shadowsocks v0.2.7 // indirect + github.com/metacubex/sing-shadowsocks2 v0.2.1 // indirect + github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d // indirect + github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect github.com/metacubex/utls v1.6.6 // indirect @@ -76,9 +76,10 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect + github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect - github.com/sagernet/sing v0.5.0-alpha.10 // indirect + github.com/sagernet/sing v0.5.0-alpha.13 // indirect github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect github.com/sagernet/sing-shadowtls v0.1.4 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect @@ -96,13 +97,14 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect go.uber.org/mock v0.4.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect diff --git a/core/go.sum b/core/go.sum index a33af65..58a3252 100644 --- a/core/go.sum +++ b/core/go.sum @@ -7,8 +7,6 @@ github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4 github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= @@ -90,8 +88,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -108,6 +106,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= +github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc= @@ -118,14 +118,14 @@ github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiL github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew= github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= -github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= -github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= -github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= -github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8= -github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e h1:o+zohxPRo45P35fS9u1zfdBgr+L/7S0ObGU6YjbVBIc= -github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM= -github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ= -github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= +github.com/metacubex/sing-shadowsocks v0.2.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g= +github.com/metacubex/sing-shadowsocks v0.2.7/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= +github.com/metacubex/sing-shadowsocks2 v0.2.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg= +github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= +github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog= +github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE= +github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I= +github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0= github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo= github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c= @@ -166,13 +166,15 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.5.0-alpha.10 h1:kuHl10gpjbKQAdQfyogQU3u0CVnpqC3wrAHe/+BFaXc= -github.com/sagernet/sing v0.5.0-alpha.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.5.0-alpha.13 h1:fpR4TFZfu/9V3LbHSAnnnwcaXGMF8ijmAAPoY2WHSKw= +github.com/sagernet/sing v0.5.0-alpha.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14= github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= @@ -218,6 +220,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= +gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= @@ -257,8 +261,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/core/hub.go b/core/hub.go index fe1076b..e6d6c6b 100644 --- a/core/hub.go +++ b/core/hub.go @@ -13,8 +13,7 @@ import ( "github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/utils" - "github.com/metacubex/mihomo/component/geodata" - "github.com/metacubex/mihomo/component/mmdb" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" cp "github.com/metacubex/mihomo/constant/provider" @@ -114,7 +113,11 @@ func updateConfig(s *C.char, port C.longlong) { configParams = params.Params prof := decorationConfig(params.ProfilePath, params.Config) currentConfig = prof - applyConfig() + err = applyConfig() + if err != nil { + bridge.SendToPort(i, err.Error()) + return + } bridge.SendToPort(i, "") }() } @@ -164,35 +167,33 @@ func getProxies() *C.char { //export changeProxy func changeProxy(s *C.char) { paramsString := C.GoString(s) - go func() { - var params = &ChangeProxyParams{} - err := json.Unmarshal([]byte(paramsString), params) - if err != nil { - log.Infoln("Unmarshal ChangeProxyParams %v", err) - } - groupName := *params.GroupName - proxyName := *params.ProxyName - proxies := tunnel.ProxiesWithProviders() - group, ok := proxies[groupName] - if !ok { - return - } - adapterProxy := group.(*adapter.Proxy) - selector, ok := adapterProxy.ProxyAdapter.(*outboundgroup.Selector) - if !ok { - return - } + var params = &ChangeProxyParams{} + err := json.Unmarshal([]byte(paramsString), params) + if err != nil { + log.Infoln("Unmarshal ChangeProxyParams %v", err) + } + groupName := *params.GroupName + proxyName := *params.ProxyName + proxies := tunnel.ProxiesWithProviders() + group, ok := proxies[groupName] + if !ok { + return + } + adapterProxy := group.(*adapter.Proxy) + selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble) + if !ok { + return + } - err = selector.Set(proxyName) - if err == nil { - log.Infoln("[Selector] %s selected %s", groupName, proxyName) - } - }() + err = selector.Set(proxyName) + if err == nil { + log.Infoln("[Selector] %s selected %s", groupName, proxyName) + } } //export getTraffic func getTraffic() *C.char { - up, down := statistic.DefaultManager.Now() + up, down := statistic.DefaultManager.Current(state.OnlyProxy) traffic := map[string]int64{ "up": up, "down": down, @@ -207,7 +208,7 @@ func getTraffic() *C.char { //export getTotalTraffic func getTotalTraffic() *C.char { - up, down := statistic.DefaultManager.Total() + up, down := statistic.DefaultManager.Total(state.OnlyProxy) traffic := map[string]int64{ "up": up, "down": down, @@ -397,25 +398,25 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l return } case "MMDB": - err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString)) + err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return } case "ASN": - err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString)) + err := updater.UpdateASN(constant.Path.Resolve(providerNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return } case "GeoIp": - err := geodata.DownloadGeoIP(constant.Path.Resolve(providerNameString)) + err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return } case "GeoSite": - err := geodata.DownloadGeoSite(constant.Path.Resolve(providerNameString)) + err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return diff --git a/core/status.go b/core/state.go similarity index 68% rename from core/status.go rename to core/state.go index 40a2f4e..c71a4ab 100644 --- a/core/status.go +++ b/core/state.go @@ -1,5 +1,3 @@ -//go:build android - package main import "C" @@ -21,11 +19,17 @@ type AndroidProps struct { SystemProxy bool `json:"systemProxy"` } -var androidProps AndroidProps +type State struct { + AndroidProps + MixedPort int `json:"mixedPort"` + OnlyProxy bool `json:"onlyProxy"` +} -//export getAndroidProps -func getAndroidProps() *C.char { - data, err := json.Marshal(androidProps) +var state State + +//export getState +func getState() *C.char { + data, err := json.Marshal(state) if err != nil { fmt.Println("Error:", err) return C.CString("") @@ -33,10 +37,10 @@ func getAndroidProps() *C.char { return C.CString(string(data)) } -//export setAndroidProps -func setAndroidProps(s *C.char) { +//export setState +func setState(s *C.char) { paramsString := C.GoString(s) - err := json.Unmarshal([]byte(paramsString), &androidProps) + err := json.Unmarshal([]byte(paramsString), &state) if err != nil { return } diff --git a/lib/application.dart b/lib/application.dart index e253318..9a24da9 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -128,7 +128,7 @@ class ApplicationState extends State { globalState.groupsUpdateTimer ??= Timer.periodic( httpTimeoutDuration, (timer) async { - await globalState.appController.updateGroups(); + await globalState.appController.updateGroupDebounce(); }, ); } @@ -136,7 +136,7 @@ class ApplicationState extends State { @override Widget build(context) { return AppStateContainer( - child: ClashMessageContainer( + child: ClashContainer( child: Selector2( selector: (_, appState, config) => ApplicationSelectorState( locale: config.locale, diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 3ff7828..895241a 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -128,8 +128,7 @@ class ClashCore { UsedProxy.GLOBAL.name, ...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) { final proxy = proxies[e] ?? {}; - return GroupTypeExtension.valueList.contains(proxy['type']) && - proxy['hidden'] != true; + return GroupTypeExtension.valueList.contains(proxy['type']); }) ]; final groupsRaw = groupNames.map((groupName) { @@ -142,7 +141,11 @@ class ClashCore { .toList(); return group; }).toList(); - return groupsRaw.map((e) => Group.fromJson(e)).toList(); + return groupsRaw + .map( + (e) => Group.fromJson(e), + ) + .toList(); }); } @@ -237,19 +240,19 @@ class ClashCore { return VersionInfo.fromJson(versionInfo); } - setProps(Props props) { - final propsChar = json.encode(props).toNativeUtf8().cast(); - clashFFI.setAndroidProps(propsChar); - malloc.free(propsChar); + setState(CoreState state) { + final stateChar = json.encode(state).toNativeUtf8().cast(); + clashFFI.setState(stateChar); + malloc.free(stateChar); } - Props getProps() { - final androidPropsRaw = clashFFI.getAndroidProps(); - final androidProps = json.decode( - androidPropsRaw.cast().toDartString(), + CoreState getState() { + final stateRaw = clashFFI.getState(); + final state = json.decode( + stateRaw.cast().toDartString(), ); - clashFFI.freeCString(androidPropsRaw); - return Props.fromJson(androidProps); + clashFFI.freeCString(stateRaw); + return CoreState.fromJson(state); } Traffic getTraffic() { diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index 7a655e9..f7c3778 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -5499,29 +5499,28 @@ class ClashFFI { late final _setProcessMap = _setProcessMapPtr.asFunction)>(); - ffi.Pointer getAndroidProps() { - return _getAndroidProps(); + ffi.Pointer getState() { + return _getState(); } - late final _getAndroidPropsPtr = - _lookup Function()>>( - 'getAndroidProps'); - late final _getAndroidProps = - _getAndroidPropsPtr.asFunction Function()>(); + late final _getStatePtr = + _lookup Function()>>('getState'); + late final _getState = + _getStatePtr.asFunction Function()>(); - void setAndroidProps( + void setState( ffi.Pointer s, ) { - return _setAndroidProps( + return _setState( s, ); } - late final _setAndroidPropsPtr = + late final _setStatePtr = _lookup)>>( - 'setAndroidProps'); - late final _setAndroidProps = - _setAndroidPropsPtr.asFunction)>(); + 'setState'); + late final _setState = + _setStatePtr.asFunction)>(); void startTUN( int fd, diff --git a/lib/common/constant.dart b/lib/common/constant.dart index f46946e..a2d2e1d 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -8,7 +8,7 @@ import 'system.dart'; const appName = "FlClash"; const coreName = "clash.meta"; -const packageName = "FlClash"; +const packageName = "com.follow.clash"; const httpTimeoutDuration = Duration(milliseconds: 5000); const moreDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100); diff --git a/lib/common/request.dart b/lib/common/request.dart index 5ae7b20..857be5c 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -85,7 +85,7 @@ class Request { "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, }; - Future checkIp(CancelToken? cancelToken) async { + Future checkIp({CancelToken? cancelToken}) async { for (final source in _ipInfoSources.entries) { try { final response = await _dio diff --git a/lib/controller.dart b/lib/controller.dart index 8eb1c7c..9883d54 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; @@ -17,6 +18,7 @@ class AppController { late ClashConfig clashConfig; late Measure measure; late Function updateClashConfigDebounce; + late Function updateGroupDebounce; late Function addCheckIpNumDebounce; AppController(this.context) { @@ -29,6 +31,9 @@ class AppController { addCheckIpNumDebounce = debounce(() { appState.checkIpNum++; }); + updateGroupDebounce = debounce(() async { + await updateGroups(); + }); measure = Measure.of(context); } @@ -45,12 +50,15 @@ class AppController { updateRunTime, updateTraffic, ]; + if (Platform.isAndroid) return; + await applyProfile(isPrue: true); } else { await globalState.stopSystemProxy(); clashCore.resetTraffic(); appState.traffics = []; appState.totalTraffic = Traffic(); appState.runTime = null; + addCheckIpNumDebounce(); } } @@ -108,24 +116,25 @@ class AppController { ); } - Future applyProfile() async { - final commonScaffoldState = globalState.homeScaffoldKey.currentState; - if (commonScaffoldState?.mounted != true) return; - commonScaffoldState?.loadingRun(() async { + Future applyProfile({bool isPrue = false}) async { + if (isPrue) { await globalState.applyProfile( appState: appState, config: config, clashConfig: clashConfig, ); - }); - } - - Future rawApplyProfile() async { - await globalState.applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ); + } else { + final commonScaffoldState = globalState.homeScaffoldKey.currentState; + if (commonScaffoldState?.mounted != true) return; + await commonScaffoldState?.loadingRun(() async { + await globalState.applyProfile( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + }); + } + addCheckIpNumDebounce(); } changeProfile(String? value) async { @@ -192,8 +201,19 @@ class AppController { await preferences.saveClashConfig(clashConfig); } + changeProxy({ + required String groupName, + required String proxyName, + }) { + globalState.changeProxy( + config: config, + groupName: groupName, + proxyName: proxyName, + ); + addCheckIpNumDebounce(); + } + handleBackOrExit() async { - print(config.isMinimizeOnExit); if (config.isMinimizeOnExit) { if (system.isDesktop) { await savePreferences(); @@ -384,6 +404,10 @@ class AppController { addProfileFormFile() async { final platformFile = await globalState.safeRun(picker.pickerConfigFile); + final bytes = platformFile?.bytes; + if (bytes == null) { + return null; + } if (!context.mounted) return; globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst); toProfiles(); @@ -392,10 +416,6 @@ class AppController { final profile = await commonScaffoldState?.loadingRun( () async { await Future.delayed(const Duration(milliseconds: 300)); - final bytes = platformFile?.bytes; - if (bytes == null) { - return null; - } return await Profile.normal(label: platformFile?.name).saveFile(bytes); }, title: "${appLocalizations.add}${appLocalizations.profile}", @@ -456,6 +476,8 @@ class AppController { String getCurrentSelectedName(String groupName) { final group = appState.getGroupWithName(groupName); - return config.currentSelectedMap[groupName] ?? group?.now ?? ''; + return group?.getCurrentSelectedName( + config.currentSelectedMap[groupName] ?? '') ?? + ''; } } diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 1f402a1..4054d6a 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -111,49 +111,49 @@ class _AccessFragmentState extends State { ); } - Widget _buildSelectedAllButton({ - required bool isAccessControl, - required bool isSelectedAll, - required List allValueList, - }) { - final tooltip = isSelectedAll - ? appLocalizations.cancelSelectAll - : appLocalizations.selectAll; - return AbsorbPointer( - absorbing: !isAccessControl, - child: FloatingActionButton( - tooltip: tooltip, - onPressed: () { - final config = globalState.appController.config; - final isAccept = - config.accessControl.mode == AccessControlMode.acceptSelected; - - if (isSelectedAll) { - config.accessControl = switch (isAccept) { - true => config.accessControl.copyWith( - acceptList: [], - ), - false => config.accessControl.copyWith( - rejectList: [], - ), - }; - } else { - config.accessControl = switch (isAccept) { - true => config.accessControl.copyWith( - acceptList: allValueList, - ), - false => config.accessControl.copyWith( - rejectList: allValueList, - ), - }; - } - }, - child: isSelectedAll - ? const Icon(Icons.deselect) - : const Icon(Icons.select_all), - ), - ); - } + // Widget _buildSelectedAllButton({ + // required bool isAccessControl, + // required bool isSelectedAll, + // required List allValueList, + // }) { + // final tooltip = isSelectedAll + // ? appLocalizations.cancelSelectAll + // : appLocalizations.selectAll; + // return AbsorbPointer( + // absorbing: !isAccessControl, + // child: FloatingActionButton( + // tooltip: tooltip, + // onPressed: () { + // final config = globalState.appController.config; + // final isAccept = + // config.accessControl.mode == AccessControlMode.acceptSelected; + // + // if (isSelectedAll) { + // config.accessControl = switch (isAccept) { + // true => config.accessControl.copyWith( + // acceptList: [], + // ), + // false => config.accessControl.copyWith( + // rejectList: [], + // ), + // }; + // } else { + // config.accessControl = switch (isAccept) { + // true => config.accessControl.copyWith( + // acceptList: allValueList, + // ), + // false => config.accessControl.copyWith( + // rejectList: allValueList, + // ), + // }; + // } + // }, + // child: isSelectedAll + // ? const Icon(Icons.deselect) + // : const Icon(Icons.select_all), + // ), + // ); + // } Widget _buildPackageList() { return ValueListenableBuilder( @@ -207,139 +207,130 @@ class _AccessFragmentState extends State { : appLocalizations.accessControlNotAllowDesc; return DisabledMask( status: !isAccessControl, - child: FloatLayout( - floatingWidget: FloatWrapper( - child: _buildSelectedAllButton( - isAccessControl: isAccessControl, - isSelectedAll: valueList.length == packageNameList.length, - allValueList: packageNameList, - ), - ), - child: Column( - children: [ - AbsorbPointer( - absorbing: !isAccessControl, - child: Padding( - padding: const EdgeInsets.only( - top: 4, - bottom: 4, - left: 16, - right: 8, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Row( - children: [ - Flexible( - child: Text( - appLocalizations.selected, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), + child: Column( + children: [ + AbsorbPointer( + absorbing: !isAccessControl, + child: Padding( + padding: const EdgeInsets.only( + top: 4, + bottom: 4, + left: 16, + right: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Flexible( + child: Text( + appLocalizations.selected, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, ), ), - const Flexible( - child: SizedBox( - width: 8, + ), + const Flexible( + child: SizedBox( + width: 8, + ), + ), + Flexible( + child: Text( + "${valueList.length}", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, ), ), - Flexible( - child: Text( - "${valueList.length}", - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - ], - ), + ), + ], ), - Flexible( - child: Text(describe), - ) - ], - ), + ), + Flexible( + child: Text(describe), + ) + ], ), ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - child: _buildSearchButton(currentPackages)), - Flexible(child: _buildFilterSystemAppButton()), - Flexible(child: _buildAppProxyModePopup()), - ], - ), - ], - ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: _buildSearchButton(currentPackages)), + Flexible(child: _buildFilterSystemAppButton()), + Flexible(child: _buildAppProxyModePopup()), + ], + ), + ], ), ), - Expanded( - flex: 1, - child: FadeBox( - key: const Key("fade_box"), - child: currentPackages.isEmpty - ? const Center( - child: CircularProgressIndicator(), - ) - : ListView.builder( - itemCount: currentPackages.length, - itemBuilder: (_, index) { - final package = currentPackages[index]; - return PackageListItem( - key: Key(package.packageName), - package: package, - value: - valueList.contains(package.packageName), - isActive: isAccessControl, - onChanged: (value) { - if (value == true) { - valueList.add(package.packageName); - } else { - valueList.remove(package.packageName); - } - final config = - globalState.appController.config; - if (accessControlMode == - AccessControlMode.acceptSelected) { - config.accessControl = - config.accessControl.copyWith( - acceptList: valueList, - ); - } else { - config.accessControl = - config.accessControl.copyWith( - rejectList: valueList, - ); - } - }, - ); - }, - ), + ), + Expanded( + flex: 1, + child: FadeBox( + key: const Key("fade_box"), + child: currentPackages.isEmpty + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: currentPackages.length, + itemBuilder: (_, index) { + final package = currentPackages[index]; + return PackageListItem( + key: Key(package.packageName), + package: package, + value: + valueList.contains(package.packageName), + isActive: isAccessControl, + onChanged: (value) { + if (value == true) { + valueList.add(package.packageName); + } else { + valueList.remove(package.packageName); + } + final config = + globalState.appController.config; + if (accessControlMode == + AccessControlMode.acceptSelected) { + config.accessControl = + config.accessControl.copyWith( + acceptList: valueList, + ); + } else { + config.accessControl = + config.accessControl.copyWith( + rejectList: valueList, + ); + } + }, + ); + }, ), ), - ], - ), + ), + ], ), ); }, diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index 14f6f78..cddf298 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -195,23 +195,40 @@ class _ConfigFragmentState extends State { }, ), Selector( - selector: (_, config) => config.isCompatible, - builder: (_, isCompatible, __) { + selector: (_, config) => config.onlyProxy, + builder: (_, onlyProxy, __) { return ListItem.switchItem( - leading: const Icon(Icons.expand_outlined), - title: Text(appLocalizations.compatible), - subtitle: Text(appLocalizations.compatibleDesc), + leading: const Icon(Icons.data_usage_outlined), + title: Text(appLocalizations.onlyStatisticsProxy), + subtitle: Text(appLocalizations.onlyStatisticsProxyDesc), delegate: SwitchDelegate( - value: isCompatible, + value: onlyProxy, onChanged: (bool value) async { final appController = globalState.appController; - appController.config.isCompatible = value; - await appController.applyProfile(); + appController.config.onlyProxy = value; }, ), ); }, ), + // Selector( + // selector: (_, config) => config.isCompatible, + // builder: (_, isCompatible, __) { + // return ListItem.switchItem( + // leading: const Icon(Icons.expand_outlined), + // title: Text(appLocalizations.compatible), + // subtitle: Text(appLocalizations.compatibleDesc), + // delegate: SwitchDelegate( + // value: isCompatible, + // onChanged: (bool value) async { + // final appController = globalState.appController; + // appController.config.isCompatible = value; + // await appController.applyProfile(); + // }, + // ), + // ); + // }, + // ), ], ); } diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart index fbdbc99..f997e3c 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -51,18 +51,6 @@ class _ConnectionsFragmentState extends State { final commonScaffoldState = context.findAncestorStateOfType(); commonScaffoldState?.actions = [ - IconButton( - onPressed: () { - clashCore.closeConnections(); - connectionsNotifier.value = connectionsNotifier.value.copyWith( - connections: clashCore.getConnections(), - ); - }, - icon: const Icon(Icons.delete_sweep_outlined), - ), - const SizedBox( - width: 8, - ), IconButton( onPressed: () { showSearch( @@ -74,6 +62,18 @@ class _ConnectionsFragmentState extends State { }, icon: const Icon(Icons.search), ), + const SizedBox( + width: 8, + ), + IconButton( + onPressed: () { + clashCore.closeConnections(); + connectionsNotifier.value = connectionsNotifier.value.copyWith( + connections: clashCore.getConnections(), + ); + }, + icon: const Icon(Icons.delete_sweep_outlined), + ), ]; }, ); diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index b14485c..b656045 100644 --- a/lib/fragments/dashboard/network_detection.dart +++ b/lib/fragments/dashboard/network_detection.dart @@ -18,48 +18,37 @@ class _NetworkDetectionState extends State { final ipInfoNotifier = ValueNotifier(null); final timeoutNotifier = ValueNotifier(false); bool? _preIsStart; - CancelToken? cancelToken; Function? _checkIpDebounce; - _checkIp( - bool isInit, - bool isStart, - ) async { + _checkIp() async { + final appState = globalState.appController.appState; + final isInit = appState.isInit; + final isStart = appState.isStart; if (!isInit) return; timeoutNotifier.value = false; if (_preIsStart == false && _preIsStart == isStart) return; - if (cancelToken != null) { - cancelToken!.cancel(); - cancelToken = null; - } + _preIsStart = isStart; ipInfoNotifier.value = null; - final ipInfo = await request.checkIp(cancelToken); + final ipInfo = await request.checkIp(); if (ipInfo == null) { timeoutNotifier.value = true; return; } else { timeoutNotifier.value = false; } - _preIsStart = isStart; ipInfoNotifier.value = ipInfo; } _checkIpContainer(Widget child) { - _checkIpDebounce = debounce(_checkIp); - return Selector2( - selector: (_, appState, config) { - return CheckIpSelectorState( - isInit: appState.isInit, - selectedMap: appState.selectedMap, - isStart: appState.isStart, - checkIpNum: appState.checkIpNum, - ); + return Selector( + selector: (_, appState) { + return appState.checkIpNum; }, - builder: (_, state, __) { + builder: (_, checkIpNum, child) { if (_checkIpDebounce != null) { - _checkIpDebounce!([state.isInit, state.isStart]); + _checkIpDebounce!(); } - return child; + return child!; }, child: child, ); @@ -74,6 +63,7 @@ class _NetworkDetectionState extends State { @override Widget build(BuildContext context) { + _checkIpDebounce = debounce(_checkIp); return _checkIpContainer( ValueListenableBuilder( valueListenable: ipInfoNotifier, diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 1e99efe..fe6d25a 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -1,10 +1,15 @@ +import 'dart:io'; +import 'dart:typed_data'; + 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/plugins/app.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; class EditProfile extends StatefulWidget { final Profile profile; @@ -26,6 +31,8 @@ class _EditProfileState extends State { late TextEditingController autoUpdateDurationController; late bool autoUpdate; final GlobalKey _formKey = GlobalKey(); + final fileInfoNotifier = ValueNotifier(null); + Uint8List? fileData; @override void initState() { @@ -36,12 +43,16 @@ class _EditProfileState extends State { autoUpdateDurationController = TextEditingController( text: widget.profile.autoUpdateDuration.inMinutes.toString(), ); + appPath.getProfilePath(widget.profile.id).then((path) async { + if (path == null) return; + fileInfoNotifier.value = await _getFileInfo(path); + }); } - _handleConfirm() { + _handleConfirm() async { if (!_formKey.currentState!.validate()) return; final config = widget.context.read(); - final profile = widget.profile.copyWith( + var profile = widget.profile.copyWith( url: urlController.text, label: labelController.text, autoUpdate: autoUpdate, @@ -52,7 +63,11 @@ class _EditProfileState extends State { ), ); final hasUpdate = widget.profile.url != profile.url; - config.setProfile(profile); + if (fileData != null) { + config.setProfile(await profile.saveFile(fileData!)); + } else { + config.setProfile(profile); + } if (hasUpdate) { globalState.homeScaffoldKey.currentState?.loadingRun( () async { @@ -62,7 +77,9 @@ class _EditProfileState extends State { }, ); } - Navigator.of(context).pop(); + if (mounted) { + Navigator.of(context).pop(); + } } _setAutoUpdate(bool value) { @@ -72,6 +89,97 @@ class _EditProfileState extends State { }); } + Future _getFileInfo(path) async { + final file = File(path); + final lastModified = await file.lastModified(); + final size = await file.length(); + return FileInfo( + size: size, + lastModified: lastModified, + ); + } + + _editProfileFile() async { + final profilePath = await appPath.getProfilePath(widget.profile.id); + if (profilePath == null) return; + globalState.safeRun(() async { + if (Platform.isAndroid) { + await app?.openFile( + profilePath, + ); + return; + } + await launchUrl( + Uri.file( + profilePath, + ), + ); + }); + } + + _uploadProfileFile() async { + final platformFile = await globalState.safeRun(picker.pickerConfigFile); + if (platformFile?.bytes == null) return; + fileData = platformFile?.bytes; + fileInfoNotifier.value = fileInfoNotifier.value?.copyWith( + size: fileData?.length ?? 0, + lastModified: DateTime.now(), + ); + } + + Widget _buildSubtitle() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + ValueListenableBuilder( + valueListenable: fileInfoNotifier, + builder: (_, fileInfo, __) { + final height = + globalState.appController.measure.bodyMediumHeight + 4; + return SizedBox( + height: height, + child: FadeBox( + child: fileInfo == null + ? SizedBox( + width: height, + height: height, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text( + fileInfo.desc, + ), + ), + ); + }, + ), + const SizedBox( + height: 8, + ), + Wrap( + runSpacing: 6, + spacing: 12, + children: [ + CommonChip( + avatar: const Icon(Icons.edit), + label: appLocalizations.edit, + onPressed: _editProfileFile, + ), + CommonChip( + avatar: const Icon(Icons.upload), + label: appLocalizations.upload, + onPressed: _uploadProfileFile, + ), + ], + ), + ], + ); + } + @override Widget build(BuildContext context) { final items = [ @@ -141,7 +249,11 @@ class _EditProfileState extends State { }, ), ), - ] + ], + ListItem( + title: Text(appLocalizations.profile), + subtitle: _buildSubtitle(), + ), ]; return FloatLayout( floatingWidget: FloatWrapper( @@ -158,17 +270,23 @@ class _EditProfileState extends State { padding: const EdgeInsets.symmetric( vertical: 16, ), - child: ListView.separated( - primary: true, - itemBuilder: (_, index) { - return items[index]; - }, - separatorBuilder: (_, __) { - return const SizedBox( - height: 24, + child: ScrollOverBuilder( + builder: (isOver) { + return ListView.separated( + padding: kMaterialListPadding.copyWith( + bottom: isOver ? 72 : 36, + ), + itemBuilder: (_, index) { + return items[index]; + }, + separatorBuilder: (_, __) { + return const SizedBox( + height: 24, + ); + }, + itemCount: items.length, ); }, - itemCount: items.length, ), ), ), diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index e9f0b64..5c8d218 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -1,6 +1,5 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart'; -import 'package:fl_clash/fragments/profiles/view_profile.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; @@ -16,7 +15,6 @@ enum ProfileActions { edit, update, delete, - view, } class ProfilesFragment extends StatefulWidget { @@ -27,7 +25,6 @@ class ProfilesFragment extends StatefulWidget { } class _ProfilesFragmentState extends State { - final hasPadding = ValueNotifier(false); Function? applyConfigDebounce; List> profileItemKeys = []; @@ -77,17 +74,6 @@ class _ProfilesFragmentState extends State { ); } - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - hasPadding.dispose(); - } - _changeProfile(String? id) async { final appController = globalState.appController; final config = appController.config; @@ -140,41 +126,33 @@ class _ProfilesFragmentState extends State { final columns = _getColumns(state.viewMode); return Align( alignment: Alignment.topCenter, - child: NotificationListener( - onNotification: (scrollNotification) { - hasPadding.value = - scrollNotification.metrics.maxScrollExtent > 0; - return true; - }, - child: ValueListenableBuilder( - valueListenable: hasPadding, - builder: (_, hasPadding, __) { - return SingleChildScrollView( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: 16 + (hasPadding ? 72 : 0), - ), - child: Grid( - mainAxisSpacing: 16, - crossAxisSpacing: 16, - crossAxisCount: columns, - children: [ - for (int i = 0; i < state.profiles.length; i++) - GridItem( - child: ProfileItem( - key: profileItemKeys[i], - profile: state.profiles[i], - groupValue: state.currentProfileId, - onChanged: _changeProfile, - ), + child: ScrollOverBuilder( + builder: (isOver) { + return SingleChildScrollView( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 16 + (isOver ? 72 : 0), + ), + child: Grid( + mainAxisSpacing: 16, + crossAxisSpacing: 16, + crossAxisCount: columns, + children: [ + for (int i = 0; i < state.profiles.length; i++) + GridItem( + child: ProfileItem( + key: profileItemKeys[i], + profile: state.profiles[i], + groupValue: state.currentProfileId, + onChanged: _changeProfile, ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ), ); }, @@ -204,7 +182,18 @@ class _ProfileItemState extends State { final isUpdating = ValueNotifier(false); _handleDeleteProfile() async { - globalState.appController.deleteProfile(widget.profile.id); + globalState.showMessage( + title: appLocalizations.tip, + message: TextSpan( + text: appLocalizations.deleteProfileTip, + ), + onTab: () async { + await globalState.appController.deleteProfile(widget.profile.id); + if(mounted){ + Navigator.of(context).pop(); + } + }, + ); } _handleUpdateProfile() async { @@ -216,9 +205,8 @@ class _ProfileItemState extends State { try { final appController = globalState.appController; await appController.updateProfile(widget.profile); - if (widget.profile.id == appController.config.currentProfile?.id && - !appController.appState.isStart) { - globalState.appController.rawApplyProfile(); + if (widget.profile.id == appController.config.currentProfile?.id) { + globalState.appController.applyProfile(isPrue: true); } } catch (e) { isUpdating.value = false; @@ -243,16 +231,6 @@ class _ProfileItemState extends State { ); } - _handleViewProfile() { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ViewProfile( - profile: widget.profile, - ), - ), - ); - } - _buildTitle(Profile profile) { final textTheme = context.textTheme; return Container( @@ -315,82 +293,12 @@ class _ProfileItemState extends State { children: [ Text( expireShow, - style: textTheme.labelMedium?.toLighter, + style: textTheme.labelMedium?.toLight, ), ], ) ], ); - // final child = switch (userInfo != null) { - // true => () { - // final use = userInfo!.upload + userInfo.download; - // final total = userInfo.total; - // final useShow = TrafficValue(value: use).show; - // final totalShow = TrafficValue(value: total).show; - // final progress = total == 0 ? 0.0 : use / total; - // final expireShow = userInfo.expire == 0 - // ? appLocalizations.infiniteTime - // : DateTime.fromMillisecondsSinceEpoch( - // userInfo.expire * 1000) - // .show; - // return Column( - // mainAxisSize: MainAxisSize.min, - // crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Container( - // margin: const EdgeInsets.symmetric( - // vertical: 8, - // ), - // child: LinearProgressIndicator( - // minHeight: 6, - // value: progress, - // ), - // ), - // Text( - // "$useShow / $totalShow", - // style: textTheme.labelMedium?.toLight(), - // ), - // const SizedBox( - // height: 2, - // ), - // Row( - // children: [ - // Text( - // appLocalizations.expirationTime, - // style: textTheme.labelMedium?.toLighter(), - // ), - // const SizedBox( - // width: 4, - // ), - // Text( - // expireShow, - // style: textTheme.labelMedium?.toLighter(), - // ), - // ], - // ) - // ], - // ); - // }(), - // false => Column( - // children: [ - // Padding( - // padding: const EdgeInsets.only(top: 8), - // child: CommonChip( - // onPressed: _handleViewProfile, - // avatar: const Icon(Icons.remove_red_eye), - // label: appLocalizations.view, - // ), - // ), - // ], - // ), - // }; - // final measure = globalState.appController.measure; - // final height = 6 + 8 * 2 + 2 + measure.labelMediumHeight * 2; - // return SizedBox( - // height: height, - // child: child, - // ); }), ], ), @@ -409,70 +317,62 @@ class _ProfileItemState extends State { final groupValue = widget.groupValue; final onChanged = widget.onChanged; return CommonCard( - child: ListItem.radio( + isSelected: profile.id == groupValue, + onPressed: () { + onChanged(profile.id); + }, + child: ListItem( key: Key(profile.id), horizontalTitleGap: 16, - delegate: RadioDelegate( - value: profile.id, - groupValue: groupValue, - onChanged: onChanged, - ), padding: const EdgeInsets.symmetric(horizontal: 16), trailing: SizedBox( - height: 48, - width: 48, + height: 40, + width: 40, child: ValueListenableBuilder( valueListenable: isUpdating, builder: (_, isUpdating, ___) { return FadeBox( - child: isUpdating - ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ) - : CommonPopupMenu( - items: [ + child: isUpdating + ? const Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : CommonPopupMenu( + items: [ + CommonPopupMenuItem( + action: ProfileActions.edit, + label: appLocalizations.edit, + iconData: Icons.edit, + ), + if (profile.type == ProfileType.url) CommonPopupMenuItem( - action: ProfileActions.edit, - label: appLocalizations.edit, - iconData: Icons.edit, + action: ProfileActions.update, + label: appLocalizations.update, + iconData: Icons.sync, ), - if (profile.type == ProfileType.url) - CommonPopupMenuItem( - action: ProfileActions.update, - label: appLocalizations.update, - iconData: Icons.sync, - ), - CommonPopupMenuItem( - action: ProfileActions.view, - label: appLocalizations.view, - iconData: Icons.visibility, - ), - CommonPopupMenuItem( - action: ProfileActions.delete, - label: appLocalizations.delete, - iconData: Icons.delete, - ), - ], - onSelected: (ProfileActions? action) async { - switch (action) { - case ProfileActions.edit: - _handleShowEditExtendPage(); - break; - case ProfileActions.delete: - _handleDeleteProfile(); - break; - case ProfileActions.update: - _handleUpdateProfile(); - break; - case ProfileActions.view: - _handleViewProfile(); - break; - case null: - break; - } - }, - )); + CommonPopupMenuItem( + action: ProfileActions.delete, + label: appLocalizations.delete, + iconData: Icons.delete, + ), + ], + onSelected: (ProfileActions? action) async { + switch (action) { + case ProfileActions.edit: + _handleShowEditExtendPage(); + break; + case ProfileActions.delete: + _handleDeleteProfile(); + break; + case ProfileActions.update: + _handleUpdateProfile(); + break; + case null: + break; + } + }, + ), + ); }, ), ), diff --git a/lib/fragments/profiles/view_profile.dart b/lib/fragments/profiles/view_profile.dart deleted file mode 100644 index 8adc8a3..0000000 --- a/lib/fragments/profiles/view_profile.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'dart:io'; - -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/scaffold.dart'; -import 'package:flutter/material.dart'; -import 'package:re_editor/re_editor.dart'; -import 'package:re_highlight/languages/yaml.dart'; -import 'package:re_highlight/styles/intellij-light.dart'; - -class ViewProfile extends StatefulWidget { - final Profile profile; - - const ViewProfile({ - super.key, - required this.profile, - }); - - @override - State createState() => _ViewProfileState(); -} - -class _ViewProfileState extends State { - bool readOnly = true; - CodeLineEditingController? controller; - final contentNotifier = ValueNotifier(""); - final key = GlobalKey(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - final profilePath = await appPath.getProfilePath(widget.profile.id); - if (profilePath == null) { - return; - } - final file = File(profilePath); - final text = await file.readAsString(); - contentNotifier.value = text; - }); - } - - @override - void dispose() { - super.dispose(); - contentNotifier.dispose(); - controller?.dispose(); - } - - Profile get profile => widget.profile; - - _handleChangeReadOnly() async { - if (readOnly == true) { - setState(() { - readOnly = false; - }); - } else { - final text = controller?.text; - if (text == null || text == contentNotifier.value) { - setState(() { - readOnly = true; - }); - return; - } - contentNotifier.value = text; - final newProfile = await key.currentState?.loadingRun(() async { - return await profile.saveFileWithString(text); - }); - if (newProfile == null) return; - globalState.appController.config.setProfile(newProfile); - setState(() { - readOnly = true; - }); - } - } - - @override - Widget build(BuildContext context) { - return CommonScaffold( - key: key, - actions: [ - IconButton( - onPressed: controller?.undo, - icon: const Icon(Icons.undo), - ), - IconButton( - onPressed: controller?.redo, - icon: const Icon(Icons.redo), - ), - if (!widget.profile.realAutoUpdate) - IconButton( - onPressed: _handleChangeReadOnly, - icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save), - ), - const SizedBox( - width: 8, - ) - ], - body: ValueListenableBuilder( - valueListenable: contentNotifier, - builder: (_, value, __) { - if (value.isEmpty) return Container(); - controller = CodeLineEditingController.fromText(value); - return CodeEditor( - autofocus: false, - readOnly: readOnly, - scrollbarBuilder: (context, child, details) { - return Scrollbar( - controller: details.controller, - thickness: 8, - radius: const Radius.circular(2), - interactive: true, - child: child, - ); - }, - showCursorWhenReadOnly: false, - controller: controller, - toolbarController: - !readOnly ? const ContextMenuControllerImpl() : null, - shortcutsActivatorsBuilder: - const DefaultCodeShortcutsActivatorsBuilder(), - indicatorBuilder: - (context, editingController, chunkController, notifier) { - return Row( - children: [ - DefaultCodeLineNumber( - controller: editingController, - notifier: notifier, - ), - DefaultCodeChunkIndicator( - width: 20, - controller: chunkController, - notifier: notifier, - ) - ], - ); - }, - style: CodeEditorStyle( - fontSize: 14, - codeTheme: CodeHighlightTheme( - languages: { - 'yaml': CodeHighlightThemeMode( - mode: langYaml, - ) - }, - theme: intellijLightTheme, - ), - ), - ); - }, - ), - title: widget.profile.label ?? widget.profile.id, - ); - } -} - -class ContextMenuItemWidget extends PopupMenuItem { - ContextMenuItemWidget({ - super.key, - required String text, - required VoidCallback super.onTap, - }) : super(child: Text(text)); -} - -class ContextMenuControllerImpl implements SelectionToolbarController { - const ContextMenuControllerImpl(); - - @override - void hide(BuildContext context) {} - - @override - void show({ - required BuildContext context, - required CodeLineEditingController controller, - required TextSelectionToolbarAnchors anchors, - Rect? renderRect, - required LayerLink layerLink, - required ValueNotifier visibility, - }) { - if (controller.selectedText.isEmpty) { - return; - } - showMenu( - context: context, - position: RelativeRect.fromSize( - (anchors.secondaryAnchor ?? anchors.primaryAnchor) & - const Size(150, double.infinity), - MediaQuery.of(context).size, - ), - items: [ - ContextMenuItemWidget( - text: appLocalizations.cut, - onTap: controller.cut, - ), - ContextMenuItemWidget( - text: appLocalizations.copy, - onTap: controller.copy, - ), - ContextMenuItemWidget( - text: appLocalizations.paste, - onTap: controller.paste, - ), - ], - ); - } -} diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index d58971d..3023817 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -1,6 +1,6 @@ -import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/fragments/proxies/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; @@ -10,7 +10,7 @@ import 'package:provider/provider.dart'; class ProxyCard extends StatelessWidget { final String groupName; final Proxy proxy; - final bool isSelected; + final GroupType groupType; final CommonCardType style; final ProxyCardType type; @@ -18,7 +18,7 @@ class ProxyCard extends StatelessWidget { super.key, required this.groupName, required this.proxy, - required this.isSelected, + required this.groupType, this.style = CommonCardType.plain, required this.type, }); @@ -91,24 +91,31 @@ class ProxyCard extends StatelessWidget { } } - _changeProxy(BuildContext context) { + _changeProxy(BuildContext context) async { final appController = globalState.appController; - final group = appController.appState.getGroupWithName(groupName)!; - if (group.type != GroupType.Selector) { - globalState.showSnackBar( - context, - message: appLocalizations.notSelectedTip, + final isUrlTest = groupType == GroupType.URLTest; + final isSelector = groupType == GroupType.Selector; + if (isUrlTest || isSelector) { + final currentProxyName = + appController.config.currentSelectedMap[groupName]; + final nextProxyName = switch (isUrlTest) { + true => currentProxyName == proxy.name ? "" : proxy.name, + false => proxy.name, + }; + appController.config.updateCurrentSelectedMap( + groupName, + nextProxyName, ); + appController.changeProxy( + groupName: groupName, + proxyName: nextProxyName, + ); + await appController.updateGroupDebounce(); return; } - globalState.appController.config.updateCurrentSelectedMap( - groupName, - proxy.name, - ); - globalState.changeProxy( - config: appController.config, - groupName: groupName, - proxyName: proxy.name, + globalState.showSnackBar( + context, + message: appLocalizations.notSelectedTip, ); } @@ -117,75 +124,125 @@ class ProxyCard extends StatelessWidget { final measure = globalState.appController.measure; final delayText = _buildDelayText(); final proxyNameText = _buildProxyNameText(context); - return CommonCard( - type: style, - key: key, - onPressed: () { - _changeProxy(context); - }, - isSelected: isSelected, - child: Container( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + return currentGroupProxyNameBuilder( + groupName: groupName, + builder: (currentGroupName) { + return Stack( children: [ - proxyNameText, - const SizedBox( - height: 8, - ), - if (type == ProxyCardType.expand) ...[ - SizedBox( - height: measure.bodySmallHeight, - child: Selector( - selector: (context, appState) => appState.getDesc( - proxy.type, - proxy.name, - ), - builder: (_, desc, __) { - return TooltipText( - text: Text( - desc, - style: context.textTheme.bodySmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: context.textTheme.bodySmall?.color?.toLight(), - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - delayText, - ] else - SizedBox( - height: measure.bodySmallHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, + CommonCard( + type: style, + key: key, + onPressed: () { + _changeProxy(context); + }, + isSelected: currentGroupName == proxy.name, + child: Container( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( - flex: 1, - child: TooltipText( - text: Text( - proxy.type, - style: context.textTheme.bodySmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: - context.textTheme.bodySmall?.color?.toLight(), + proxyNameText, + const SizedBox( + height: 8, + ), + if (type == ProxyCardType.expand) ...[ + SizedBox( + height: measure.bodySmallHeight, + child: Selector( + selector: (context, appState) => appState.getDesc( + proxy.type, + proxy.name, ), + builder: (_, desc, __) { + return TooltipText( + text: Text( + desc, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: context.textTheme.bodySmall?.color + ?.toLight(), + ), + ), + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + delayText, + ] else + SizedBox( + height: measure.bodySmallHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + flex: 1, + child: TooltipText( + text: Text( + proxy.type, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: context.textTheme.bodySmall?.color + ?.toLight(), + ), + ), + ), + ), + delayText, + ], ), ), - ), - delayText, ], ), ), + ), + if (groupType == GroupType.URLTest) + Selector( + selector: (_, config) { + final selectedProxyName = + config.currentSelectedMap[groupName]; + return selectedProxyName ?? ''; + }, + builder: (_, value, __) { + if (value != proxy.name) return Container(); + return Positioned.fill( + child: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.all(8), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + Theme.of(context).colorScheme.secondaryContainer, + ), + child: const SelectIcon(), + ), + ), + ); + }, + child: Positioned.fill( + child: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.all(8), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: const SelectIcon(), + ), + ), + ), + ) ], - ), - ), + ); + }, ); } } diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart index 31453b5..629d0dd 100644 --- a/lib/fragments/proxies/common.dart +++ b/lib/fragments/proxies/common.dart @@ -8,17 +8,18 @@ import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -Widget currentProxyNameBuilder({ +Widget currentGroupProxyNameBuilder({ required String groupName, - required Widget Function(String) builder, + required Widget Function(String currentGroupName) builder, }) { return Selector2( selector: (_, appState, config) { final group = appState.getGroupWithName(groupName); - return config.currentSelectedMap[groupName] ?? group?.now ?? ''; + final selectedProxyName = config.currentSelectedMap[groupName]; + return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? ""; }, - builder: (_, value, ___) { - return builder(value); + builder: (_, currentGroupName, ___) { + return builder(currentGroupName); }, ); } @@ -42,8 +43,7 @@ double getItemHeight(ProxyCardType proxyCardType) { delayTest(List proxies) async { final appController = globalState.appController; for (final proxy in proxies) { - final proxyName = - appController.appState.getRealProxyName(proxy.name) ?? proxy.name; + final proxyName = appController.appState.getRealProxyName(proxy.name); globalState.appController.setDelay( Delay( name: proxyName, @@ -70,6 +70,6 @@ double getScrollToSelectedOffset({ (proxy) => proxy.name == selectedName, ); final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0; - final rows = ((selectedIndex - 1) / columns).ceil(); + final rows = (selectedIndex / columns).floor(); return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0); } diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index 1ce6955..3c8c5ff 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -140,17 +140,13 @@ class _ProxiesListFragmentState extends State { final children = proxies .map( (proxy) => Flexible( - child: currentProxyNameBuilder( - groupName: group.name, - builder: (currentProxyName) { - return ProxyCard( - type: type, - isSelected: currentProxyName == proxy.name, - key: ValueKey('$groupName.${proxy.name}'), - proxy: proxy, - groupName: groupName, - ); - }), + child: ProxyCard( + type: type, + groupType: group.type, + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ), ), ) .fill( @@ -209,6 +205,9 @@ class _ProxiesListFragmentState extends State { } _scrollToGroupSelected(String groupName) { + if (_controller.position.maxScrollExtent == 0) { + return; + } final appController = globalState.appController; final currentGroups = appController.appState.currentGroups; final groupNames = currentGroups.map((e) => e.name).toList(); @@ -393,6 +392,18 @@ class _ListHeaderState extends State super.dispose(); } + @override + void didUpdateWidget(ListHeader oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isExpand != widget.isExpand) { + if (isExpand) { + _animationController.value = 1.0; + } else { + _animationController.value = 0.0; + } + } + } + @override Widget build(BuildContext context) { return CommonCard( @@ -410,9 +421,7 @@ class _ListHeaderState extends State children: [ Text( groupName, - style: context.textTheme.titleMedium?.copyWith( - color: context.colorScheme.primary, - ), + style: context.textTheme.titleMedium, ), const SizedBox( height: 4, @@ -430,20 +439,20 @@ class _ListHeaderState extends State ), Flexible( flex: 1, - child: currentProxyNameBuilder( + child: currentGroupProxyNameBuilder( groupName: groupName, - builder: (value) { + builder: (currentGroupName) { return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (value.isNotEmpty) ...[ + if (currentGroupName.isNotEmpty) ...[ Flexible( flex: 1, child: Text( overflow: TextOverflow.ellipsis, - " ยท $value", + " ยท $currentGroupName", style: context .textTheme.labelMedium?.toLight, ), diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index f9fd68c..d8a26b6 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -262,54 +262,10 @@ class ProxyGroupViewState extends State { _controller.dispose(); } - Widget _buildTabGroupView({ - required List proxies, - required int columns, - required ProxyCardType proxyCardType, - }) { - final sortedProxies = globalState.appController.getSortProxies( - proxies, - ); - _lastProxies = sortedProxies; - return DelayTestButtonContainer( - onClick: () async { - await _delayTest( - proxies, - ); - }, - child: Align( - alignment: Alignment.topCenter, - child: GridView.builder( - controller: _controller, - padding: const EdgeInsets.all(16), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: getItemHeight(proxyCardType), - ), - itemCount: sortedProxies.length, - itemBuilder: (_, index) { - final proxy = sortedProxies[index]; - return currentProxyNameBuilder( - builder: (value) { - return ProxyCard( - type: proxyCardType, - key: ValueKey('$groupName.${proxy.name}'), - isSelected: value == proxy.name, - proxy: proxy, - groupName: groupName, - ); - }, - groupName: groupName, - ); - }, - ), - ), - ); - } - scrollToSelected() { + if (_controller.position.maxScrollExtent == 0) { + return; + } _controller.animateTo( 16 + getScrollToSelectedOffset( @@ -332,16 +288,47 @@ class ProxyGroupViewState extends State { columns: globalState.appController.columns, sortNum: appState.sortNum, proxies: group.all, + groupType: group.type, ); }, builder: (_, state, __) { final proxies = state.proxies; final columns = state.columns; final proxyCardType = state.proxyCardType; - return _buildTabGroupView( - proxies: proxies, - columns: columns, - proxyCardType: proxyCardType, + final sortedProxies = globalState.appController.getSortProxies( + proxies, + ); + _lastProxies = sortedProxies; + return DelayTestButtonContainer( + onClick: () async { + await _delayTest( + proxies, + ); + }, + child: Align( + alignment: Alignment.topCenter, + child: GridView.builder( + controller: _controller, + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: getItemHeight(proxyCardType), + ), + itemCount: sortedProxies.length, + itemBuilder: (_, index) { + final proxy = sortedProxies[index]; + return ProxyCard( + groupType: state.groupType, + type: proxyCardType, + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ); + }, + ), + ), ); }, ); diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index c3c0be0..0bbda9d 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -22,17 +22,6 @@ class GeoItem { }); } -@immutable -class FileInfo { - final String size; - final DateTime lastModified; - - const FileInfo({ - required this.size, - required this.lastModified, - }); -} - class Resources extends StatefulWidget { const Resources({super.key}); @@ -196,27 +185,11 @@ class _GeoDataListItemState extends State { final lastModified = await file.lastModified(); final size = await file.length(); return FileInfo( - size: TrafficValue(value: size).show, + size: size, lastModified: lastModified, ); } - // _uploadGeoFile(String fileName) async { - // final res = await picker.pickerGeoDataFile(); - // if (res == null || res.bytes == null) return; - // final homePath = await appPath.getHomeDirPath(); - // final file = File(join(homePath, fileName)); - // await file.writeAsBytes( - // res.bytes!, - // flush: true, - // ); - // setState(() {}); - // } - - String _buildFileInfoDesc(FileInfo fileInfo) { - return "${fileInfo.size} ยท ${fileInfo.lastModified.lastUpdateTimeDesc}"; - } - Widget _buildSubtitle(String url) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -240,7 +213,7 @@ class _GeoDataListItemState extends State { ), ) : Text( - _buildFileInfoDesc(snapshot.data!), + snapshot.data!.desc, ), ), ); @@ -510,4 +483,4 @@ class _UpdateGeoUrlFormDialogState extends State { ], ); } -} \ No newline at end of file +} diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index ccadef4..9d78ad8 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -216,5 +216,8 @@ "externalLink": "External link", "otherContributors": "Other contributors", "autoCloseConnections": "Auto lose connections", - "autoCloseConnectionsDesc": "Auto close connections after change node" + "autoCloseConnectionsDesc": "Auto close connections after change node", + "onlyStatisticsProxy": "Only statistics proxy", + "onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic", + "deleteProfileTip": "Sure you want to delete the current profile?" } \ 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 65c600e..e3f2955 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -216,5 +216,8 @@ "externalLink": "ๅค–้ƒจ้“พๆŽฅ", "otherContributors": "ๅ…ถไป–่ดก็Œฎ่€…", "autoCloseConnections": "่‡ชๅŠจๅ…ณ้—ญ่ฟžๆŽฅ", - "autoCloseConnectionsDesc": "ๅˆ‡ๆข่Š‚็‚นๅŽ่‡ชๅŠจๅ…ณ้—ญ่ฟžๆŽฅ" + "autoCloseConnectionsDesc": "ๅˆ‡ๆข่Š‚็‚นๅŽ่‡ชๅŠจๅ…ณ้—ญ่ฟžๆŽฅ", + "onlyStatisticsProxy": "ไป…็ปŸ่ฎกไปฃ็†", + "onlyStatisticsProxyDesc": "ๅผ€ๅฏๅŽ๏ผŒๅฐ†ๅช็ปŸ่ฎกไปฃ็†ๆต้‡", + "deleteProfileTip": "็กฎๅฎš่ฆๅˆ ้™คๅฝ“ๅ‰้…็ฝฎๅ—?" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index fbdc730..4e19795 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -115,6 +115,8 @@ class MessageLookup extends MessageLookupByLibrary { "delay": MessageLookupByLibrary.simpleMessage("Delay"), "delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"), "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "deleteProfileTip": MessageLookupByLibrary.simpleMessage( + "Sure you want to delete the current profile?"), "desc": MessageLookupByLibrary.simpleMessage( "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free."), "direct": MessageLookupByLibrary.simpleMessage("Direct"), @@ -208,6 +210,10 @@ class MessageLookup extends MessageLookupByLibrary { "No profile, Please add a profile"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), "oneColumn": MessageLookupByLibrary.simpleMessage("One column"), + "onlyStatisticsProxy": + MessageLookupByLibrary.simpleMessage("Only statistics proxy"), + "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( + "When turned on, only statistics proxy traffic"), "other": MessageLookupByLibrary.simpleMessage("Other"), "otherContributors": MessageLookupByLibrary.simpleMessage("Other contributors"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 8190eb7..63f5e4c 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -96,6 +96,7 @@ class MessageLookup extends MessageLookupByLibrary { "delay": MessageLookupByLibrary.simpleMessage("ๅปถ่ฟŸ"), "delaySort": MessageLookupByLibrary.simpleMessage("ๆŒ‰ๅปถ่ฟŸๆŽ’ๅบ"), "delete": MessageLookupByLibrary.simpleMessage("ๅˆ ้™ค"), + "deleteProfileTip": MessageLookupByLibrary.simpleMessage("็กฎๅฎš่ฆๅˆ ้™คๅฝ“ๅ‰้…็ฝฎๅ—?"), "desc": MessageLookupByLibrary.simpleMessage( "ๅŸบไบŽClashMeta็š„ๅคšๅนณๅฐไปฃ็†ๅฎขๆˆท็ซฏ๏ผŒ็ฎ€ๅ•ๆ˜“็”จ๏ผŒๅผ€ๆบๆ— ๅนฟๅ‘Šใ€‚"), "direct": MessageLookupByLibrary.simpleMessage("็›ด่ฟž"), @@ -170,6 +171,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ๆฒกๆœ‰้…็ฝฎๆ–‡ไปถ,่ฏทๅ…ˆๆทปๅŠ ้…็ฝฎๆ–‡ไปถ"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("ๆš‚ๆ— ่ฏทๆฑ‚"), "oneColumn": MessageLookupByLibrary.simpleMessage("ไธ€ๅˆ—"), + "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("ไป…็ปŸ่ฎกไปฃ็†"), + "onlyStatisticsProxyDesc": + MessageLookupByLibrary.simpleMessage("ๅผ€ๅฏๅŽ๏ผŒๅฐ†ๅช็ปŸ่ฎกไปฃ็†ๆต้‡"), "other": MessageLookupByLibrary.simpleMessage("ๅ…ถไป–"), "otherContributors": MessageLookupByLibrary.simpleMessage("ๅ…ถไป–่ดก็Œฎ่€…"), "outboundMode": MessageLookupByLibrary.simpleMessage("ๅ‡บ็ซ™ๆจกๅผ"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 8637f27..e256048 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2229,6 +2229,36 @@ class AppLocalizations { args: [], ); } + + /// `Only statistics proxy` + String get onlyStatisticsProxy { + return Intl.message( + 'Only statistics proxy', + name: 'onlyStatisticsProxy', + desc: '', + args: [], + ); + } + + /// `When turned on, only statistics proxy traffic` + String get onlyStatisticsProxyDesc { + return Intl.message( + 'When turned on, only statistics proxy traffic', + name: 'onlyStatisticsProxyDesc', + desc: '', + args: [], + ); + } + + /// `Sure you want to delete the current profile?` + String get deleteProfileTip { + return Intl.message( + 'Sure you want to delete the current profile?', + name: 'deleteProfileTip', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/models/app.dart b/lib/models/app.dart index 609a1ad..081f649 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -55,7 +55,7 @@ class AppState with ChangeNotifier { _delayMap = {}, _groups = [], _isCompatible = isCompatible, - _systemColorSchemes = SystemColorSchemes(); + _systemColorSchemes = const SystemColorSchemes(); String get currentLabel => _currentLabel; @@ -109,7 +109,7 @@ class AppState with ChangeNotifier { } } - String getDesc(String type, String? proxyName) { + String getDesc(String type, String proxyName) { final groupTypeNamesList = GroupType.values.map((e) => e.name).toList(); if (!groupTypeNamesList.contains(type)) { return type; @@ -120,15 +120,17 @@ class AppState with ChangeNotifier { } } - String? getRealProxyName(String? proxyName) { - if (proxyName == null) return null; + String getRealProxyName(String proxyName) { + if (proxyName.isEmpty) return proxyName; final index = groups.indexWhere((element) => element.name == proxyName); if (index == -1) return proxyName; final group = groups[index]; - return getRealProxyName((selectedMap.containsKey(proxyName) - ? selectedMap[proxyName] - : group.now)) ?? - proxyName; + final currentSelectedName = + group.getCurrentSelectedName(selectedMap[proxyName] ?? ''); + if (currentSelectedName.isEmpty) return proxyName; + return getRealProxyName( + currentSelectedName, + ); } String? get showProxyName { @@ -140,7 +142,7 @@ class AppState with ChangeNotifier { return selectedMap[firstGroupName] ?? firstGroup.now; } - int? getDelay(String? proxyName) { + int? getDelay(String proxyName) { return _delayMap[getRealProxyName(proxyName)]; } @@ -293,6 +295,7 @@ class AppState with ChangeNotifier { .toList(); case Mode.rule: return groups + .where((item) => item.hidden == false) .where((element) => element.name != GroupName.GLOBAL.name) .toList(); } diff --git a/lib/models/config.dart b/lib/models/config.dart index b097b37..30239fb 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -26,14 +26,16 @@ class AccessControl with _$AccessControl { } @freezed -class Props with _$Props { - const factory Props({ +class CoreState with _$CoreState { + const factory CoreState({ AccessControl? accessControl, required bool allowBypass, required bool systemProxy, - }) = _Props; + required int mixedPort, + required bool onlyProxy, + }) = _CoreState; - factory Props.fromJson(Map json) => _$PropsFromJson(json); + factory CoreState.fromJson(Map json) => _$CoreStateFromJson(json); } @freezed @@ -79,6 +81,7 @@ class Config extends ChangeNotifier { int _proxiesColumns; String _testUrl; WindowProps _windowProps; + bool _onlyProxy; Config() : _profiles = [], @@ -103,7 +106,8 @@ class Config extends ChangeNotifier { _proxyCardType = ProxyCardType.expand, _windowProps = defaultWindowProps, _proxiesType = ProxiesType.tab, - _proxiesColumns = 2; + _proxiesColumns = 2, + _onlyProxy = false; deleteProfileById(String id) { _profiles = profiles.where((element) => element.id != id).toList(); @@ -407,6 +411,19 @@ class Config extends ChangeNotifier { } } + @JsonKey(defaultValue: false) + bool get onlyProxy { + return _onlyProxy; + } + + set onlyProxy(bool value) { + if (_onlyProxy != value) { + _onlyProxy = value; + notifyListeners(); + } + } + + @JsonKey(defaultValue: false) bool get isCloseConnections { return _isCloseConnections; diff --git a/lib/models/file.dart b/lib/models/file.dart new file mode 100644 index 0000000..0c3b97e --- /dev/null +++ b/lib/models/file.dart @@ -0,0 +1,21 @@ +import 'package:fl_clash/common/datetime.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'traffic.dart'; + +part 'generated/file.freezed.dart'; + +@freezed +class FileInfo with _$FileInfo { + const factory FileInfo({ + required int size, + required DateTime lastModified, + }) = _FileInfo; +} + + +extension FileInfoExt on FileInfo{ + String get desc => "${TrafficValue(value: size).show} ยท ${lastModified.lastUpdateTimeDesc}"; +} + + diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index de29e2a..b708a27 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -240,35 +240,43 @@ abstract class _AccessControl implements AccessControl { throw _privateConstructorUsedError; } -Props _$PropsFromJson(Map json) { - return _Props.fromJson(json); +CoreState _$CoreStateFromJson(Map json) { + return _CoreState.fromJson(json); } /// @nodoc -mixin _$Props { +mixin _$CoreState { AccessControl? get accessControl => throw _privateConstructorUsedError; bool get allowBypass => throw _privateConstructorUsedError; bool get systemProxy => throw _privateConstructorUsedError; + int get mixedPort => throw _privateConstructorUsedError; + bool get onlyProxy => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $PropsCopyWith get copyWith => throw _privateConstructorUsedError; + $CoreStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $PropsCopyWith<$Res> { - factory $PropsCopyWith(Props value, $Res Function(Props) then) = - _$PropsCopyWithImpl<$Res, Props>; +abstract class $CoreStateCopyWith<$Res> { + factory $CoreStateCopyWith(CoreState value, $Res Function(CoreState) then) = + _$CoreStateCopyWithImpl<$Res, CoreState>; @useResult - $Res call({AccessControl? accessControl, bool allowBypass, bool systemProxy}); + $Res call( + {AccessControl? accessControl, + bool allowBypass, + bool systemProxy, + int mixedPort, + bool onlyProxy}); $AccessControlCopyWith<$Res>? get accessControl; } /// @nodoc -class _$PropsCopyWithImpl<$Res, $Val extends Props> - implements $PropsCopyWith<$Res> { - _$PropsCopyWithImpl(this._value, this._then); +class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> + implements $CoreStateCopyWith<$Res> { + _$CoreStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -281,6 +289,8 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props> Object? accessControl = freezed, Object? allowBypass = null, Object? systemProxy = null, + Object? mixedPort = null, + Object? onlyProxy = null, }) { return _then(_value.copyWith( accessControl: freezed == accessControl @@ -295,6 +305,14 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props> ? _value.systemProxy : systemProxy // ignore: cast_nullable_to_non_nullable as bool, + mixedPort: null == mixedPort + ? _value.mixedPort + : mixedPort // ignore: cast_nullable_to_non_nullable + as int, + onlyProxy: null == onlyProxy + ? _value.onlyProxy + : onlyProxy // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -312,24 +330,30 @@ class _$PropsCopyWithImpl<$Res, $Val extends Props> } /// @nodoc -abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> { - factory _$$PropsImplCopyWith( - _$PropsImpl value, $Res Function(_$PropsImpl) then) = - __$$PropsImplCopyWithImpl<$Res>; +abstract class _$$CoreStateImplCopyWith<$Res> + implements $CoreStateCopyWith<$Res> { + factory _$$CoreStateImplCopyWith( + _$CoreStateImpl value, $Res Function(_$CoreStateImpl) then) = + __$$CoreStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({AccessControl? accessControl, bool allowBypass, bool systemProxy}); + $Res call( + {AccessControl? accessControl, + bool allowBypass, + bool systemProxy, + int mixedPort, + bool onlyProxy}); @override $AccessControlCopyWith<$Res>? get accessControl; } /// @nodoc -class __$$PropsImplCopyWithImpl<$Res> - extends _$PropsCopyWithImpl<$Res, _$PropsImpl> - implements _$$PropsImplCopyWith<$Res> { - __$$PropsImplCopyWithImpl( - _$PropsImpl _value, $Res Function(_$PropsImpl) _then) +class __$$CoreStateImplCopyWithImpl<$Res> + extends _$CoreStateCopyWithImpl<$Res, _$CoreStateImpl> + implements _$$CoreStateImplCopyWith<$Res> { + __$$CoreStateImplCopyWithImpl( + _$CoreStateImpl _value, $Res Function(_$CoreStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -338,8 +362,10 @@ class __$$PropsImplCopyWithImpl<$Res> Object? accessControl = freezed, Object? allowBypass = null, Object? systemProxy = null, + Object? mixedPort = null, + Object? onlyProxy = null, }) { - return _then(_$PropsImpl( + return _then(_$CoreStateImpl( accessControl: freezed == accessControl ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable @@ -352,20 +378,30 @@ class __$$PropsImplCopyWithImpl<$Res> ? _value.systemProxy : systemProxy // ignore: cast_nullable_to_non_nullable as bool, + mixedPort: null == mixedPort + ? _value.mixedPort + : mixedPort // ignore: cast_nullable_to_non_nullable + as int, + onlyProxy: null == onlyProxy + ? _value.onlyProxy + : onlyProxy // ignore: cast_nullable_to_non_nullable + as bool, )); } } /// @nodoc @JsonSerializable() -class _$PropsImpl implements _Props { - const _$PropsImpl( +class _$CoreStateImpl implements _CoreState { + const _$CoreStateImpl( {this.accessControl, required this.allowBypass, - required this.systemProxy}); + required this.systemProxy, + required this.mixedPort, + required this.onlyProxy}); - factory _$PropsImpl.fromJson(Map json) => - _$$PropsImplFromJson(json); + factory _$CoreStateImpl.fromJson(Map json) => + _$$CoreStateImplFromJson(json); @override final AccessControl? accessControl; @@ -373,51 +409,62 @@ class _$PropsImpl implements _Props { final bool allowBypass; @override final bool systemProxy; + @override + final int mixedPort; + @override + final bool onlyProxy; @override String toString() { - return 'Props(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy)'; + return 'CoreState(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$PropsImpl && + other is _$CoreStateImpl && (identical(other.accessControl, accessControl) || other.accessControl == accessControl) && (identical(other.allowBypass, allowBypass) || other.allowBypass == allowBypass) && (identical(other.systemProxy, systemProxy) || - other.systemProxy == systemProxy)); + other.systemProxy == systemProxy) && + (identical(other.mixedPort, mixedPort) || + other.mixedPort == mixedPort) && + (identical(other.onlyProxy, onlyProxy) || + other.onlyProxy == onlyProxy)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, accessControl, allowBypass, systemProxy); + int get hashCode => Object.hash(runtimeType, accessControl, allowBypass, + systemProxy, mixedPort, onlyProxy); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$PropsImplCopyWith<_$PropsImpl> get copyWith => - __$$PropsImplCopyWithImpl<_$PropsImpl>(this, _$identity); + _$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith => + __$$CoreStateImplCopyWithImpl<_$CoreStateImpl>(this, _$identity); @override Map toJson() { - return _$$PropsImplToJson( + return _$$CoreStateImplToJson( this, ); } } -abstract class _Props implements Props { - const factory _Props( +abstract class _CoreState implements CoreState { + const factory _CoreState( {final AccessControl? accessControl, required final bool allowBypass, - required final bool systemProxy}) = _$PropsImpl; + required final bool systemProxy, + required final int mixedPort, + required final bool onlyProxy}) = _$CoreStateImpl; - factory _Props.fromJson(Map json) = _$PropsImpl.fromJson; + factory _CoreState.fromJson(Map json) = + _$CoreStateImpl.fromJson; @override AccessControl? get accessControl; @@ -426,8 +473,12 @@ abstract class _Props implements Props { @override bool get systemProxy; @override + int get mixedPort; + @override + bool get onlyProxy; + @override @JsonKey(ignore: true) - _$$PropsImplCopyWith<_$PropsImpl> get copyWith => + _$$CoreStateImplCopyWith<_$CoreStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index e626e54..ff6962f 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -35,6 +35,7 @@ Config _$ConfigFromJson(Map json) => Config() ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true ..allowBypass = json['allowBypass'] as bool? ?? true ..systemProxy = json['systemProxy'] as bool? ?? false + ..onlyProxy = json['onlyProxy'] as bool? ?? false ..isCloseConnections = json['isCloseConnections'] as bool? ?? false ..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'], unknownValue: ProxiesType.tab) ?? @@ -69,6 +70,7 @@ Map _$ConfigToJson(Config instance) => { 'autoCheckUpdate': instance.autoCheckUpdate, 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, + 'onlyProxy': instance.onlyProxy, 'isCloseConnections': instance.isCloseConnections, 'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!, 'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!, @@ -129,20 +131,25 @@ const _$AccessControlModeEnumMap = { AccessControlMode.rejectSelected: 'rejectSelected', }; -_$PropsImpl _$$PropsImplFromJson(Map json) => _$PropsImpl( +_$CoreStateImpl _$$CoreStateImplFromJson(Map json) => + _$CoreStateImpl( accessControl: json['accessControl'] == null ? null : AccessControl.fromJson( json['accessControl'] as Map), allowBypass: json['allowBypass'] as bool, systemProxy: json['systemProxy'] as bool, + mixedPort: (json['mixedPort'] as num).toInt(), + onlyProxy: json['onlyProxy'] as bool, ); -Map _$$PropsImplToJson(_$PropsImpl instance) => +Map _$$CoreStateImplToJson(_$CoreStateImpl instance) => { 'accessControl': instance.accessControl, 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, + 'mixedPort': instance.mixedPort, + 'onlyProxy': instance.onlyProxy, }; _$WindowPropsImpl _$$WindowPropsImplFromJson(Map json) => diff --git a/lib/models/generated/file.freezed.dart b/lib/models/generated/file.freezed.dart new file mode 100644 index 0000000..81f4da5 --- /dev/null +++ b/lib/models/generated/file.freezed.dart @@ -0,0 +1,150 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../file.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$FileInfo { + int get size => throw _privateConstructorUsedError; + DateTime get lastModified => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $FileInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FileInfoCopyWith<$Res> { + factory $FileInfoCopyWith(FileInfo value, $Res Function(FileInfo) then) = + _$FileInfoCopyWithImpl<$Res, FileInfo>; + @useResult + $Res call({int size, DateTime lastModified}); +} + +/// @nodoc +class _$FileInfoCopyWithImpl<$Res, $Val extends FileInfo> + implements $FileInfoCopyWith<$Res> { + _$FileInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? size = null, + Object? lastModified = null, + }) { + return _then(_value.copyWith( + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as int, + lastModified: null == lastModified + ? _value.lastModified + : lastModified // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$FileInfoImplCopyWith<$Res> + implements $FileInfoCopyWith<$Res> { + factory _$$FileInfoImplCopyWith( + _$FileInfoImpl value, $Res Function(_$FileInfoImpl) then) = + __$$FileInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int size, DateTime lastModified}); +} + +/// @nodoc +class __$$FileInfoImplCopyWithImpl<$Res> + extends _$FileInfoCopyWithImpl<$Res, _$FileInfoImpl> + implements _$$FileInfoImplCopyWith<$Res> { + __$$FileInfoImplCopyWithImpl( + _$FileInfoImpl _value, $Res Function(_$FileInfoImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? size = null, + Object? lastModified = null, + }) { + return _then(_$FileInfoImpl( + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as int, + lastModified: null == lastModified + ? _value.lastModified + : lastModified // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc + +class _$FileInfoImpl implements _FileInfo { + const _$FileInfoImpl({required this.size, required this.lastModified}); + + @override + final int size; + @override + final DateTime lastModified; + + @override + String toString() { + return 'FileInfo(size: $size, lastModified: $lastModified)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FileInfoImpl && + (identical(other.size, size) || other.size == size) && + (identical(other.lastModified, lastModified) || + other.lastModified == lastModified)); + } + + @override + int get hashCode => Object.hash(runtimeType, size, lastModified); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$FileInfoImplCopyWith<_$FileInfoImpl> get copyWith => + __$$FileInfoImplCopyWithImpl<_$FileInfoImpl>(this, _$identity); +} + +abstract class _FileInfo implements FileInfo { + const factory _FileInfo( + {required final int size, + required final DateTime lastModified}) = _$FileInfoImpl; + + @override + int get size; + @override + DateTime get lastModified; + @override + @JsonKey(ignore: true) + _$$FileInfoImplCopyWith<_$FileInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/proxy.freezed.dart b/lib/models/generated/proxy.freezed.dart index f963e39..d9c81dc 100644 --- a/lib/models/generated/proxy.freezed.dart +++ b/lib/models/generated/proxy.freezed.dart @@ -23,6 +23,7 @@ mixin _$Group { GroupType get type => throw _privateConstructorUsedError; List get all => throw _privateConstructorUsedError; String? get now => throw _privateConstructorUsedError; + bool? get hidden => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -35,7 +36,12 @@ abstract class $GroupCopyWith<$Res> { factory $GroupCopyWith(Group value, $Res Function(Group) then) = _$GroupCopyWithImpl<$Res, Group>; @useResult - $Res call({GroupType type, List all, String? now, String name}); + $Res call( + {GroupType type, + List all, + String? now, + bool? hidden, + String name}); } /// @nodoc @@ -54,6 +60,7 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group> Object? type = null, Object? all = null, Object? now = freezed, + Object? hidden = freezed, Object? name = null, }) { return _then(_value.copyWith( @@ -69,6 +76,10 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group> ? _value.now : now // ignore: cast_nullable_to_non_nullable as String?, + hidden: freezed == hidden + ? _value.hidden + : hidden // ignore: cast_nullable_to_non_nullable + as bool?, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -84,7 +95,12 @@ abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> { __$$GroupImplCopyWithImpl<$Res>; @override @useResult - $Res call({GroupType type, List all, String? now, String name}); + $Res call( + {GroupType type, + List all, + String? now, + bool? hidden, + String name}); } /// @nodoc @@ -101,6 +117,7 @@ class __$$GroupImplCopyWithImpl<$Res> Object? type = null, Object? all = null, Object? now = freezed, + Object? hidden = freezed, Object? name = null, }) { return _then(_$GroupImpl( @@ -116,6 +133,10 @@ class __$$GroupImplCopyWithImpl<$Res> ? _value.now : now // ignore: cast_nullable_to_non_nullable as String?, + hidden: freezed == hidden + ? _value.hidden + : hidden // ignore: cast_nullable_to_non_nullable + as bool?, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -131,6 +152,7 @@ class _$GroupImpl implements _Group { {required this.type, final List all = const [], this.now, + this.hidden, required this.name}) : _all = all; @@ -151,11 +173,13 @@ class _$GroupImpl implements _Group { @override final String? now; @override + final bool? hidden; + @override final String name; @override String toString() { - return 'Group(type: $type, all: $all, now: $now, name: $name)'; + return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, name: $name)'; } @override @@ -166,13 +190,14 @@ class _$GroupImpl implements _Group { (identical(other.type, type) || other.type == type) && const DeepCollectionEquality().equals(other._all, _all) && (identical(other.now, now) || other.now == now) && + (identical(other.hidden, hidden) || other.hidden == hidden) && (identical(other.name, name) || other.name == name)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, type, const DeepCollectionEquality().hash(_all), now, name); + int get hashCode => Object.hash(runtimeType, type, + const DeepCollectionEquality().hash(_all), now, hidden, name); @JsonKey(ignore: true) @override @@ -193,6 +218,7 @@ abstract class _Group implements Group { {required final GroupType type, final List all, final String? now, + final bool? hidden, required final String name}) = _$GroupImpl; factory _Group.fromJson(Map json) = _$GroupImpl.fromJson; @@ -204,6 +230,8 @@ abstract class _Group implements Group { @override String? get now; @override + bool? get hidden; + @override String get name; @override @JsonKey(ignore: true) diff --git a/lib/models/generated/proxy.g.dart b/lib/models/generated/proxy.g.dart index 8b3f0f0..4383835 100644 --- a/lib/models/generated/proxy.g.dart +++ b/lib/models/generated/proxy.g.dart @@ -13,6 +13,7 @@ _$GroupImpl _$$GroupImplFromJson(Map json) => _$GroupImpl( .toList() ?? const [], now: json['now'] as String?, + hidden: json['hidden'] as bool?, name: json['name'] as String, ); @@ -21,6 +22,7 @@ Map _$$GroupImplToJson(_$GroupImpl instance) => 'type': _$GroupTypeEnumMap[instance.type]!, 'all': instance.all, 'now': instance.now, + 'hidden': instance.hidden, 'name': instance.name, }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index bd40a83..f4ee0dc 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -158,10 +158,8 @@ abstract class _StartButtonSelectorState implements StartButtonSelectorState { /// @nodoc mixin _$CheckIpSelectorState { - bool get isInit => throw _privateConstructorUsedError; - bool get isStart => throw _privateConstructorUsedError; + String? get currentProfileId => throw _privateConstructorUsedError; Map get selectedMap => throw _privateConstructorUsedError; - num get checkIpNum => throw _privateConstructorUsedError; @JsonKey(ignore: true) $CheckIpSelectorStateCopyWith get copyWith => @@ -174,11 +172,7 @@ abstract class $CheckIpSelectorStateCopyWith<$Res> { $Res Function(CheckIpSelectorState) then) = _$CheckIpSelectorStateCopyWithImpl<$Res, CheckIpSelectorState>; @useResult - $Res call( - {bool isInit, - bool isStart, - Map selectedMap, - num checkIpNum}); + $Res call({String? currentProfileId, Map selectedMap}); } /// @nodoc @@ -195,28 +189,18 @@ class _$CheckIpSelectorStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? isInit = null, - Object? isStart = null, + Object? currentProfileId = freezed, Object? selectedMap = null, - Object? checkIpNum = null, }) { return _then(_value.copyWith( - isInit: null == isInit - ? _value.isInit - : isInit // ignore: cast_nullable_to_non_nullable - as bool, - isStart: null == isStart - ? _value.isStart - : isStart // ignore: cast_nullable_to_non_nullable - as bool, + currentProfileId: freezed == currentProfileId + ? _value.currentProfileId + : currentProfileId // ignore: cast_nullable_to_non_nullable + as String?, selectedMap: null == selectedMap ? _value.selectedMap : selectedMap // ignore: cast_nullable_to_non_nullable as Map, - checkIpNum: null == checkIpNum - ? _value.checkIpNum - : checkIpNum // ignore: cast_nullable_to_non_nullable - as num, ) as $Val); } } @@ -229,11 +213,7 @@ abstract class _$$CheckIpSelectorStateImplCopyWith<$Res> __$$CheckIpSelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call( - {bool isInit, - bool isStart, - Map selectedMap, - num checkIpNum}); + $Res call({String? currentProfileId, Map selectedMap}); } /// @nodoc @@ -247,28 +227,18 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? isInit = null, - Object? isStart = null, + Object? currentProfileId = freezed, Object? selectedMap = null, - Object? checkIpNum = null, }) { return _then(_$CheckIpSelectorStateImpl( - isInit: null == isInit - ? _value.isInit - : isInit // ignore: cast_nullable_to_non_nullable - as bool, - isStart: null == isStart - ? _value.isStart - : isStart // ignore: cast_nullable_to_non_nullable - as bool, + currentProfileId: freezed == currentProfileId + ? _value.currentProfileId + : currentProfileId // ignore: cast_nullable_to_non_nullable + as String?, selectedMap: null == selectedMap ? _value._selectedMap : selectedMap // ignore: cast_nullable_to_non_nullable as Map, - checkIpNum: null == checkIpNum - ? _value.checkIpNum - : checkIpNum // ignore: cast_nullable_to_non_nullable - as num, )); } } @@ -277,16 +247,12 @@ class __$$CheckIpSelectorStateImplCopyWithImpl<$Res> class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState { const _$CheckIpSelectorStateImpl( - {required this.isInit, - required this.isStart, - required final Map selectedMap, - required this.checkIpNum}) + {required this.currentProfileId, + required final Map selectedMap}) : _selectedMap = selectedMap; @override - final bool isInit; - @override - final bool isStart; + final String? currentProfileId; final Map _selectedMap; @override Map get selectedMap { @@ -295,12 +261,9 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState { return EqualUnmodifiableMapView(_selectedMap); } - @override - final num checkIpNum; - @override String toString() { - return 'CheckIpSelectorState(isInit: $isInit, isStart: $isStart, selectedMap: $selectedMap, checkIpNum: $checkIpNum)'; + return 'CheckIpSelectorState(currentProfileId: $currentProfileId, selectedMap: $selectedMap)'; } @override @@ -308,17 +271,15 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$CheckIpSelectorStateImpl && - (identical(other.isInit, isInit) || other.isInit == isInit) && - (identical(other.isStart, isStart) || other.isStart == isStart) && + (identical(other.currentProfileId, currentProfileId) || + other.currentProfileId == currentProfileId) && const DeepCollectionEquality() - .equals(other._selectedMap, _selectedMap) && - (identical(other.checkIpNum, checkIpNum) || - other.checkIpNum == checkIpNum)); + .equals(other._selectedMap, _selectedMap)); } @override - int get hashCode => Object.hash(runtimeType, isInit, isStart, - const DeepCollectionEquality().hash(_selectedMap), checkIpNum); + int get hashCode => Object.hash(runtimeType, currentProfileId, + const DeepCollectionEquality().hash(_selectedMap)); @JsonKey(ignore: true) @override @@ -331,20 +292,15 @@ class _$CheckIpSelectorStateImpl implements _CheckIpSelectorState { abstract class _CheckIpSelectorState implements CheckIpSelectorState { const factory _CheckIpSelectorState( - {required final bool isInit, - required final bool isStart, - required final Map selectedMap, - required final num checkIpNum}) = _$CheckIpSelectorStateImpl; + {required final String? currentProfileId, + required final Map selectedMap}) = + _$CheckIpSelectorStateImpl; @override - bool get isInit; - @override - bool get isStart; + String? get currentProfileId; @override Map get selectedMap; @override - num get checkIpNum; - @override @JsonKey(ignore: true) _$$CheckIpSelectorStateImplCopyWith<_$CheckIpSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; @@ -2012,6 +1968,7 @@ mixin _$ProxyGroupSelectorState { ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError; ProxyCardType get proxyCardType => throw _privateConstructorUsedError; num get sortNum => throw _privateConstructorUsedError; + GroupType get groupType => throw _privateConstructorUsedError; List get proxies => throw _privateConstructorUsedError; int get columns => throw _privateConstructorUsedError; @@ -2030,6 +1987,7 @@ abstract class $ProxyGroupSelectorStateCopyWith<$Res> { {ProxiesSortType proxiesSortType, ProxyCardType proxyCardType, num sortNum, + GroupType groupType, List proxies, int columns}); } @@ -2051,6 +2009,7 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res, Object? proxiesSortType = null, Object? proxyCardType = null, Object? sortNum = null, + Object? groupType = null, Object? proxies = null, Object? columns = null, }) { @@ -2067,6 +2026,10 @@ class _$ProxyGroupSelectorStateCopyWithImpl<$Res, ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable as num, + groupType: null == groupType + ? _value.groupType + : groupType // ignore: cast_nullable_to_non_nullable + as GroupType, proxies: null == proxies ? _value.proxies : proxies // ignore: cast_nullable_to_non_nullable @@ -2092,6 +2055,7 @@ abstract class _$$ProxyGroupSelectorStateImplCopyWith<$Res> {ProxiesSortType proxiesSortType, ProxyCardType proxyCardType, num sortNum, + GroupType groupType, List proxies, int columns}); } @@ -2112,6 +2076,7 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res> Object? proxiesSortType = null, Object? proxyCardType = null, Object? sortNum = null, + Object? groupType = null, Object? proxies = null, Object? columns = null, }) { @@ -2128,6 +2093,10 @@ class __$$ProxyGroupSelectorStateImplCopyWithImpl<$Res> ? _value.sortNum : sortNum // ignore: cast_nullable_to_non_nullable as num, + groupType: null == groupType + ? _value.groupType + : groupType // ignore: cast_nullable_to_non_nullable + as GroupType, proxies: null == proxies ? _value._proxies : proxies // ignore: cast_nullable_to_non_nullable @@ -2147,6 +2116,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState { {required this.proxiesSortType, required this.proxyCardType, required this.sortNum, + required this.groupType, required final List proxies, required this.columns}) : _proxies = proxies; @@ -2157,6 +2127,8 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState { final ProxyCardType proxyCardType; @override final num sortNum; + @override + final GroupType groupType; final List _proxies; @override List get proxies { @@ -2170,7 +2142,7 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState { @override String toString() { - return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, proxies: $proxies, columns: $columns)'; + return 'ProxyGroupSelectorState(proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, groupType: $groupType, proxies: $proxies, columns: $columns)'; } @override @@ -2183,13 +2155,21 @@ class _$ProxyGroupSelectorStateImpl implements _ProxyGroupSelectorState { (identical(other.proxyCardType, proxyCardType) || other.proxyCardType == proxyCardType) && (identical(other.sortNum, sortNum) || other.sortNum == sortNum) && + (identical(other.groupType, groupType) || + other.groupType == groupType) && const DeepCollectionEquality().equals(other._proxies, _proxies) && (identical(other.columns, columns) || other.columns == columns)); } @override - int get hashCode => Object.hash(runtimeType, proxiesSortType, proxyCardType, - sortNum, const DeepCollectionEquality().hash(_proxies), columns); + int get hashCode => Object.hash( + runtimeType, + proxiesSortType, + proxyCardType, + sortNum, + groupType, + const DeepCollectionEquality().hash(_proxies), + columns); @JsonKey(ignore: true) @override @@ -2204,6 +2184,7 @@ abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState { {required final ProxiesSortType proxiesSortType, required final ProxyCardType proxyCardType, required final num sortNum, + required final GroupType groupType, required final List proxies, required final int columns}) = _$ProxyGroupSelectorStateImpl; @@ -2214,6 +2195,8 @@ abstract class _ProxyGroupSelectorState implements ProxyGroupSelectorState { @override num get sortNum; @override + GroupType get groupType; + @override List get proxies; @override int get columns; @@ -2800,3 +2783,154 @@ abstract class _ProxiesListHeaderSelectorState _$ProxiesListHeaderSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$CurrentGroupProxyNameSelectorState { + String? get proxyName => throw _privateConstructorUsedError; + String? get proxyName2 => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $CurrentGroupProxyNameSelectorStateCopyWith< + CurrentGroupProxyNameSelectorState> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { + factory $CurrentGroupProxyNameSelectorStateCopyWith( + CurrentGroupProxyNameSelectorState value, + $Res Function(CurrentGroupProxyNameSelectorState) then) = + _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, + CurrentGroupProxyNameSelectorState>; + @useResult + $Res call({String? proxyName, String? proxyName2}); +} + +/// @nodoc +class _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, + $Val extends CurrentGroupProxyNameSelectorState> + implements $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { + _$CurrentGroupProxyNameSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? proxyName = freezed, + Object? proxyName2 = freezed, + }) { + return _then(_value.copyWith( + proxyName: freezed == proxyName + ? _value.proxyName + : proxyName // ignore: cast_nullable_to_non_nullable + as String?, + proxyName2: freezed == proxyName2 + ? _value.proxyName2 + : proxyName2 // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CurrentGroupProxyNameSelectorStateImplCopyWith<$Res> + implements $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { + factory _$$CurrentGroupProxyNameSelectorStateImplCopyWith( + _$CurrentGroupProxyNameSelectorStateImpl value, + $Res Function(_$CurrentGroupProxyNameSelectorStateImpl) then) = + __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? proxyName, String? proxyName2}); +} + +/// @nodoc +class __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl<$Res> + extends _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, + _$CurrentGroupProxyNameSelectorStateImpl> + implements _$$CurrentGroupProxyNameSelectorStateImplCopyWith<$Res> { + __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl( + _$CurrentGroupProxyNameSelectorStateImpl _value, + $Res Function(_$CurrentGroupProxyNameSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? proxyName = freezed, + Object? proxyName2 = freezed, + }) { + return _then(_$CurrentGroupProxyNameSelectorStateImpl( + proxyName: freezed == proxyName + ? _value.proxyName + : proxyName // ignore: cast_nullable_to_non_nullable + as String?, + proxyName2: freezed == proxyName2 + ? _value.proxyName2 + : proxyName2 // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$CurrentGroupProxyNameSelectorStateImpl + implements _CurrentGroupProxyNameSelectorState { + const _$CurrentGroupProxyNameSelectorStateImpl( + {required this.proxyName, required this.proxyName2}); + + @override + final String? proxyName; + @override + final String? proxyName2; + + @override + String toString() { + return 'CurrentGroupProxyNameSelectorState(proxyName: $proxyName, proxyName2: $proxyName2)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CurrentGroupProxyNameSelectorStateImpl && + (identical(other.proxyName, proxyName) || + other.proxyName == proxyName) && + (identical(other.proxyName2, proxyName2) || + other.proxyName2 == proxyName2)); + } + + @override + int get hashCode => Object.hash(runtimeType, proxyName, proxyName2); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$CurrentGroupProxyNameSelectorStateImplCopyWith< + _$CurrentGroupProxyNameSelectorStateImpl> + get copyWith => __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl< + _$CurrentGroupProxyNameSelectorStateImpl>(this, _$identity); +} + +abstract class _CurrentGroupProxyNameSelectorState + implements CurrentGroupProxyNameSelectorState { + const factory _CurrentGroupProxyNameSelectorState( + {required final String? proxyName, + required final String? proxyName2}) = + _$CurrentGroupProxyNameSelectorStateImpl; + + @override + String? get proxyName; + @override + String? get proxyName2; + @override + @JsonKey(ignore: true) + _$$CurrentGroupProxyNameSelectorStateImplCopyWith< + _$CurrentGroupProxyNameSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/models.dart b/lib/models/models.dart index 370aa3d..9532967 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -13,4 +13,5 @@ export 'ffi.dart'; export 'selector.dart'; export 'navigation.dart'; export 'dav.dart'; -export 'ip.dart'; \ No newline at end of file +export 'ip.dart'; +export 'file.dart'; \ No newline at end of file diff --git a/lib/models/proxy.dart b/lib/models/proxy.dart index ab2654a..e122454 100644 --- a/lib/models/proxy.dart +++ b/lib/models/proxy.dart @@ -14,12 +14,24 @@ class Group with _$Group { required GroupType type, @Default([]) List all, String? now, + bool? hidden, required String name, }) = _Group; factory Group.fromJson(Map json) => _$GroupFromJson(json); } +extension GroupExt on Group { + String get realNow => now ?? ""; + + String getCurrentSelectedName(String proxyName) { + if (type == GroupType.URLTest) { + return realNow.isNotEmpty ? realNow : proxyName; + } + return proxyName.isNotEmpty ? proxyName : realNow; + } +} + @freezed class Proxy with _$Proxy { const factory Proxy({ diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 83e38d0..f2e4577 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -16,10 +16,8 @@ class StartButtonSelectorState with _$StartButtonSelectorState { @freezed class CheckIpSelectorState with _$CheckIpSelectorState { const factory CheckIpSelectorState({ - required bool isInit, - required bool isStart, + required String? currentProfileId, required SelectedMap selectedMap, - required num checkIpNum }) = _CheckIpSelectorState; } @@ -117,6 +115,7 @@ class ProxyGroupSelectorState with _$ProxyGroupSelectorState { required ProxiesSortType proxiesSortType, required ProxyCardType proxyCardType, required num sortNum, + required GroupType groupType, required List proxies, required int columns, }) = _ProxyGroupSelectorState; @@ -152,4 +151,12 @@ class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState { required double offset, required int currentIndex, }) = _ProxiesListHeaderSelectorState; +} + +@freezed +class CurrentGroupProxyNameSelectorState with _$CurrentGroupProxyNameSelectorState { + const factory CurrentGroupProxyNameSelectorState({ + required String? proxyName, + required String? proxyName2, + }) = _CurrentGroupProxyNameSelectorState; } \ No newline at end of file diff --git a/lib/pages/home.dart b/lib/pages/home.dart index e86bf20..c5fde7c 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -52,6 +52,19 @@ class HomePage extends StatelessWidget { context: context, child: NavigationRail( groupAlignment: -0.8, + selectedIconTheme: IconThemeData( + color: context.colorScheme.onSurfaceVariant, + ), + unselectedIconTheme: IconThemeData( + color: context.colorScheme.onSurfaceVariant, + ), + selectedLabelTextStyle: context.textTheme.labelLarge!.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + unselectedLabelTextStyle: context.textTheme.labelLarge!.copyWith( + color: context.colorScheme.onSurface, + ), destinations: navigationItems .map( (e) => NavigationRailDestination( @@ -64,7 +77,7 @@ class HomePage extends StatelessWidget { .toList(), onDestinationSelected: globalState.appController.toPage, extended: extended, - minExtendedWidth: 172, + minExtendedWidth: 200, selectedIndex: currentIndex, labelType: extended ? NavigationRailLabelType.none diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index 3e844de..d55cdfc 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -5,10 +5,8 @@ import 'dart:isolate'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; class App { static App? _instance; @@ -50,6 +48,13 @@ class App { }); } + Future openFile(String path) async { + return await methodChannel.invokeMethod("openFile", { + "path": path, + }) ?? + false; + } + Future getPackageIcon(String packageName) async { final base64 = await methodChannel.invokeMethod("getPackageIcon", { "packageName": packageName, diff --git a/lib/plugins/proxy.dart b/lib/plugins/proxy.dart index 164e26f..ca212ad 100644 --- a/lib/plugins/proxy.dart +++ b/lib/plugins/proxy.dart @@ -47,9 +47,10 @@ class Proxy extends ProxyPlatform { @override Future startProxy(port) async { + final state = clashCore.getState(); return await methodChannel.invokeMethod("startProxy", { - 'port': port, - 'args': json.encode(clashCore.getProps()), + 'port': state.mixedPort, + 'args': json.encode(state), }); } diff --git a/lib/state.dart b/lib/state.dart index bb7265f..f64957c 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -72,13 +72,6 @@ class GlobalState { required ClashConfig clashConfig, }) async { if (!globalState.isVpnService && Platform.isAndroid) { - clashCore.setProps( - Props( - accessControl: config.isAccessControl ? config.accessControl : null, - allowBypass: config.allowBypass, - systemProxy: config.systemProxy, - ), - ); await proxy?.initService(); } else { await proxyManager.startProxy( @@ -86,16 +79,6 @@ class GlobalState { ); } startListenUpdate(); - if (Platform.isAndroid) { - return; - } - applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ).then((_) { - globalState.appController.addCheckIpNumDebounce(); - }); } Future stopSystemProxy() async { @@ -124,15 +107,6 @@ class GlobalState { }) async { appState.isInit = clashCore.isInit; if (!appState.isInit) { - if (Platform.isAndroid) { - clashCore.setProps( - Props( - accessControl: config.isAccessControl ? config.accessControl : null, - allowBypass: config.allowBypass, - systemProxy: config.systemProxy, - ), - ); - } appState.isInit = await clashService.init( config: config, clashConfig: clashConfig, @@ -160,12 +134,14 @@ class GlobalState { width: 300, constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( - child: RichText( - overflow: TextOverflow.visible, - text: TextSpan( + child: SelectableText.rich( + TextSpan( style: Theme.of(context).textTheme.labelLarge, children: [message], ), + style: const TextStyle( + overflow: TextOverflow.visible, + ), ), ), ), @@ -195,7 +171,7 @@ class GlobalState { proxyName: proxyName, ), ); - if(config.isCloseConnections){ + if (config.isCloseConnections) { clashCore.closeConnections(); } } diff --git a/lib/widgets/android_container.dart b/lib/widgets/android_container.dart index 37592c3..0111af9 100644 --- a/lib/widgets/android_container.dart +++ b/lib/widgets/android_container.dart @@ -19,7 +19,6 @@ class AndroidContainer extends StatefulWidget { class _AndroidContainerState extends State with WidgetsBindingObserver { - Widget _excludeContainer(Widget child) { return Selector( selector: (_, config) => config.isExclude, diff --git a/lib/widgets/builder.dart b/lib/widgets/builder.dart new file mode 100644 index 0000000..829439d --- /dev/null +++ b/lib/widgets/builder.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ScrollOverBuilder extends StatefulWidget { + final Widget Function(bool isOver) builder; + + const ScrollOverBuilder({ + super.key, + required this.builder, + }); + + @override + State createState() => _ScrollOverBuilderState(); +} + +class _ScrollOverBuilderState extends State { + final isOverNotifier = ValueNotifier(false); + + + @override + void dispose() { + super.dispose(); + isOverNotifier.dispose(); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (scrollNotification) { + isOverNotifier.value = scrollNotification.metrics.maxScrollExtent > 0; + return true; + }, + child: ValueListenableBuilder( + valueListenable: isOverNotifier, + builder: (_, isOver, __) { + return widget.builder(isOver); + }, + ), + ); + } +} diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 15e51ff..a743251 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -38,10 +38,7 @@ class InfoHeader extends StatelessWidget { if (info.iconData != null) ...[ Icon( info.iconData, - color: Theme - .of(context) - .colorScheme - .primary, + color: Theme.of(context).colorScheme.primary, ), const SizedBox( width: 8, @@ -53,10 +50,7 @@ class InfoHeader extends StatelessWidget { info.label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme - .of(context) - .textTheme - .titleMedium, + style: Theme.of(context).textTheme.titleMedium, ), ), ), @@ -86,6 +80,7 @@ class CommonCard extends StatelessWidget { this.onPressed, this.info, this.selectWidget, + this.radius = 12, required this.child, }) : isSelected = isSelected ?? false; @@ -95,14 +90,13 @@ class CommonCard extends StatelessWidget { final Widget child; final Info? info; final CommonCardType type; + final double radius; BorderSide getBorderSide(BuildContext context, Set states) { if (type == CommonCardType.filled) { return BorderSide.none; } - final colorScheme = Theme - .of(context) - .colorScheme; + final colorScheme = Theme.of(context).colorScheme; final hoverColor = isSelected ? colorScheme.primary.toLight() : colorScheme.primary.toLighter(); @@ -119,9 +113,7 @@ class CommonCard extends StatelessWidget { } Color? getBackgroundColor(BuildContext context, Set states) { - final colorScheme = Theme - .of(context) - .colorScheme; + final colorScheme = Theme.of(context).colorScheme; switch (type) { case CommonCardType.plain: if (isSelected) { @@ -130,8 +122,7 @@ class CommonCard extends StatelessWidget { if (states.isEmpty) { return colorScheme.secondaryContainer.toLittle(); } - return Theme - .of(context) + return Theme.of(context) .outlinedButtonTheme .style ?.backgroundColor @@ -167,29 +158,31 @@ class CommonCard extends StatelessWidget { ], ); } - return OutlinedButton( clipBehavior: Clip.antiAlias, style: ButtonStyle( padding: const WidgetStatePropertyAll(EdgeInsets.zero), shape: WidgetStatePropertyAll( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(radius), ), ), backgroundColor: WidgetStateProperty.resolveWith( - (states) => getBackgroundColor(context, states), + (states) => getBackgroundColor(context, states), ), side: WidgetStateProperty.resolveWith( - (states) => getBorderSide(context, states), + (states) => getBorderSide(context, states), ), ), onPressed: onPressed, child: Builder( builder: (_) { + if (selectWidget == null) { + return childWidget; + } List children = []; children.add(childWidget); - if (selectWidget != null && isSelected) { + if (isSelected) { children.add( Positioned.fill( child: selectWidget!, @@ -211,10 +204,7 @@ class SelectIcon extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - color: Theme - .of(context) - .colorScheme - .inversePrimary, + color: Theme.of(context).colorScheme.inversePrimary, shape: const CircleBorder(), child: Container( padding: const EdgeInsets.all(4), diff --git a/lib/widgets/clash_message_container.dart b/lib/widgets/clash_container.dart similarity index 62% rename from lib/widgets/clash_message_container.dart rename to lib/widgets/clash_container.dart index c2fae4b..c3f7aa7 100644 --- a/lib/widgets/clash_message_container.dart +++ b/lib/widgets/clash_container.dart @@ -3,24 +3,42 @@ import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class ClashMessageContainer extends StatefulWidget { +class ClashContainer extends StatefulWidget { final Widget child; - const ClashMessageContainer({ + const ClashContainer({ super.key, required this.child, }); @override - State createState() => _ClashMessageContainerState(); + State createState() => _ClashContainerState(); } -class _ClashMessageContainerState extends State +class _ClashContainerState extends State with AppMessageListener { + Widget _updateCoreState(Widget child) { + return Selector2( + selector: (_, config, clashConfig) => CoreState( + accessControl: config.isAccessControl ? config.accessControl : null, + allowBypass: config.allowBypass, + systemProxy: config.systemProxy, + mixedPort: clashConfig.mixedPort, + onlyProxy: config.onlyProxy, + ), + builder: (__, state, child) { + clashCore.setState(state); + return child!; + }, + child: child, + ); + } + @override Widget build(BuildContext context) { - return widget.child; + return _updateCoreState(widget.child); } @override @@ -60,22 +78,19 @@ class _ClashMessageContainerState extends State final currentSelectedMap = appController.config.currentSelectedMap; final proxyName = currentSelectedMap[groupName]; if (proxyName == null) return; - globalState.changeProxy( - config: appController.config, + appController.changeProxy( groupName: groupName, proxyName: proxyName, ); - appController.addCheckIpNumDebounce(); super.onLoaded(proxyName); } @override - void onStarted(String runTime) { + Future onStarted(String runTime) async { super.onStarted(runTime); proxy?.updateStartTime(); final appController = globalState.appController; - appController.rawApplyProfile().then((_) { - appController.addCheckIpNumDebounce(); - }); + await appController.applyProfile(isPrue: true); + appController.addCheckIpNumDebounce(); } } diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 0dd2c3d..46e40a0 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -213,6 +213,9 @@ class ListItem extends StatelessWidget { Widget build(BuildContext context) { if (delegate is OpenDelegate) { final openDelegate = delegate as OpenDelegate; + final child = SafeArea( + child: openDelegate.widget, + ); return OpenContainer( closedBuilder: (_, action) { openAction() { @@ -221,7 +224,7 @@ class ListItem extends StatelessWidget { if (!isMobile) { showExtendPage( context, - body: openDelegate.widget, + body: child, title: openDelegate.title, extendPageWidth: openDelegate.extendPageWidth, ); @@ -230,14 +233,16 @@ class ListItem extends StatelessWidget { action(); } - return _buildListTile(onTap: openAction); + return _buildListTile( + onTap: openAction, + ); }, openBuilder: (_, action) { return CommonScaffold.open( key: Key(openDelegate.title), onBack: action, title: openDelegate.title, - body: openDelegate.widget, + body: child, ); }, ); @@ -399,10 +404,10 @@ List generateInfoSection({ }) { final genItems = separated ? items.separated( - const Divider( - height: 0, - ), - ) + const Divider( + height: 0, + ), + ) : items; return [ if (items.isNotEmpty) @@ -414,7 +419,6 @@ List generateInfoSection({ ]; } - Widget generateListView(List items) { return ListView.builder( itemCount: items.length, diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 73b46a4..99f4c33 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -85,7 +85,7 @@ class CommonScaffoldState extends State { } @override - void didUpdateWidget(covariant CommonScaffold oldWidget) { + void didUpdateWidget(CommonScaffold oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.title != widget.title) { _actions.value = []; @@ -94,6 +94,8 @@ class CommonScaffoldState extends State { Widget? get _sideNavigationBar => widget.sideNavigationBar; + Widget get body => SafeArea(child: widget.body); + @override Widget build(BuildContext context) { final scaffold = Scaffold( @@ -107,7 +109,7 @@ class CommonScaffoldState extends State { valueListenable: _actions, builder: (_, actions, __) { final realActions = - actions.isNotEmpty ? actions : widget.actions; + actions.isNotEmpty ? actions : widget.actions; return AppBar( centerTitle: false, automaticallyImplyLeading: widget.automaticallyImplyLeading, @@ -133,7 +135,7 @@ class CommonScaffoldState extends State { ], ), ), - body: widget.body, + body: body, bottomNavigationBar: widget.bottomNavigationBar, ); return _sideNavigationBar != null diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index dee4e52..cc0432f 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -68,7 +68,13 @@ showSheet({ showModalBottomSheet( context: context, isScrollControlled: isScrollControlled, - builder: builder, + builder: (context) { + return SafeArea( + child: builder( + context, + ), + ); + }, showDragHandle: true, useSafeArea: true, ); @@ -80,7 +86,9 @@ showSheet({ constraints: BoxConstraints( maxWidth: width, ), - body: builder(context), + body: SafeArea( + child: builder(context), + ), title: title, ); } diff --git a/lib/widgets/side_sheet.dart b/lib/widgets/side_sheet.dart index d546f32..b132da7 100644 --- a/lib/widgets/side_sheet.dart +++ b/lib/widgets/side_sheet.dart @@ -589,25 +589,27 @@ Future showModalSideSheet({ final MaterialLocalizations localizations = MaterialLocalizations.of(context); return navigator.push(ModalSideSheetRoute( builder: (context) { - return Column( - children: [ - AppBar( - automaticallyImplyLeading: false, - title: Text(title), - centerTitle: false, - actions: const [ - SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: CloseButton(), - ) - ], - ), - Expanded( - flex: 1, - child: body, - ), - ], + return SafeArea( + child: Column( + children: [ + AppBar( + automaticallyImplyLeading: false, + title: Text(title), + centerTitle: false, + actions: const [ + SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: CloseButton(), + ) + ], + ), + Expanded( + flex: 1, + child: body, + ), + ], + ), ); }, capturedThemes: diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 0b22e1c..e1ab8a9 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -17,10 +17,11 @@ export 'animate_grid.dart'; export 'tray_container.dart'; export 'window_container.dart'; export 'android_container.dart'; -export 'clash_message_container.dart'; +export 'clash_container.dart'; export 'tile_container.dart'; export 'chip.dart'; export 'fade_box.dart'; export 'app_state_container.dart'; export 'text.dart'; -export 'connection_item.dart'; \ No newline at end of file +export 'connection_item.dart'; +export 'builder.dart'; \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 6c35df5..8e734a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -525,22 +525,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - isolate_contactor: - dependency: transitive - description: - name: isolate_contactor - sha256: f1be0a90f91e4309ef37cc45280b2a84e769e848aae378318dd3dd263cfc482a - url: "https://pub.dev" - source: hosted - version: "4.2.0" - isolate_manager: - dependency: transitive - description: - name: isolate_manager - sha256: "8fb916c4444fd408f089448f904f083ac3e169ea1789fd4d987b25809af92188" - url: "https://pub.dev" - source: hosted - version: "4.3.1" jovial_misc: dependency: transitive description: @@ -844,22 +828,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" - re_editor: - dependency: "direct main" - description: - name: re_editor - sha256: db7a82e95f0f74301e85d4d5c805a8b8a5ba43d6c0d26673b7e35dc011f06635 - url: "https://pub.dev" - source: hosted - version: "0.3.0" - re_highlight: - dependency: "direct main" - description: - name: re_highlight - sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2" - url: "https://pub.dev" - source: hosted - version: "0.0.3" screen_retriever: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 03a6feb..251cf76 100644 --- 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.48+202407251 +version: 0.8.49+202407311 environment: sdk: '>=3.1.0 <4.0.0'