From 13ac20f27337d183beca5a5aadc3ff3c285ded05 Mon Sep 17 00:00:00 2001 From: txyyh Date: Fri, 19 Jul 2024 21:13:42 +0800 Subject: [PATCH 01/25] Add push release files to fdroid-repo --- .github/workflows/build.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7d82e00d..8ec198471 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,4 +140,18 @@ jobs: uses: softprops/action-gh-release@v2 with: files: ./dist/* - body_path: './release.md' \ No newline at end of file + body_path: './release.md' + + - name: Push to fdroid repo + uses: cpina/github-action-push-to-another-repository@v1.7.2 + env: + SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} + with: + source-directory: ./dist/ + destination-github-username: chen08209 + destination-repository-name: flclash-fdroid-repo + user-name: 'github-actions[bot]' + user-email: 'github-actions[bot]@users.noreply.github.com' + target-branch: action-pr + commit-message: Update from ${{ github.ref_name }} + target-directory: /tmp/ From f6b97f82aea4e32b030c68c7d4f245b6f3862332 Mon Sep 17 00:00:00 2001 From: txyyh Date: Fri, 19 Jul 2024 22:49:26 +0800 Subject: [PATCH 02/25] Add download guide to README --- README.md | 4 + README_zh_CN.md | 5 + snapshots/get-it-on-fdroid.svg | 347 +++++++++++++++++++++++++++++++++ snapshots/get-it-on-github.svg | 55 ++++++ 4 files changed, 411 insertions(+) create mode 100644 snapshots/get-it-on-fdroid.svg create mode 100644 snapshots/get-it-on-github.svg diff --git a/README.md b/README.md index e03498705..51160c0df 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ on Mobile: ✨ Support subscription link, Dark mode +## Download + +Get it on F-Droid Get it on GitHub + ## Contact [Telegram](https://t.me/+G-veVtwBOl4wODc1) diff --git a/README_zh_CN.md b/README_zh_CN.md index 586d7208f..ddcd6529c 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -38,6 +38,11 @@ on Mobile: ✨ 支持一键导入订阅, 深色模式 +## Download + +Get it on F-Droid Get it on GitHub + + ## Contact [Telegram](https://t.me/+G-veVtwBOl4wODc1) diff --git a/snapshots/get-it-on-fdroid.svg b/snapshots/get-it-on-fdroid.svg new file mode 100644 index 000000000..fd54a2edf --- /dev/null +++ b/snapshots/get-it-on-fdroid.svg @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + Andrew Nayenko + + + + + https://f-droid.org + + + + + + + + + + + + + + + + + + + + + + GET IT ON + F-Droid + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snapshots/get-it-on-github.svg b/snapshots/get-it-on-github.svg new file mode 100644 index 000000000..539882494 --- /dev/null +++ b/snapshots/get-it-on-github.svg @@ -0,0 +1,55 @@ + + + + + + + + + + image/svg+xml + + + + + Andrew Nayenko + + + + + https://f-droid.org + + + + + + + + + + + + + + + + + + + + + + GET IT ON + GitHub + + + + + + + + + + + + From 3ba83557721b3ae322dad20428009edcad046cdf Mon Sep 17 00:00:00 2001 From: txyyh Date: Mon, 29 Jul 2024 16:42:25 +0800 Subject: [PATCH 03/25] Rename workflow deploy repo name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ec198471..1803f84c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,7 +149,7 @@ jobs: with: source-directory: ./dist/ destination-github-username: chen08209 - destination-repository-name: flclash-fdroid-repo + destination-repository-name: FlClash-fdroid-repo user-name: 'github-actions[bot]' user-email: 'github-actions[bot]@users.noreply.github.com' target-branch: action-pr From fcbbbdc6985782917cbc4ebbab4ffd5af0dd124e Mon Sep 17 00:00:00 2001 From: txyyh Date: Mon, 29 Jul 2024 16:45:12 +0800 Subject: [PATCH 04/25] Rename readme fingerprint --- README.md | 2 +- README_zh_CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51160c0df..deee11204 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ on Mobile: ## Download -Get it on F-Droid Get it on GitHub +Get it on F-Droid Get it on GitHub ## Contact diff --git a/README_zh_CN.md b/README_zh_CN.md index ddcd6529c..27be4b6d6 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -40,7 +40,7 @@ on Mobile: ## Download -Get it on F-Droid Get it on GitHub +Get it on F-Droid Get it on GitHub ## Contact From f39b9cf933b8827a71dd18e1d8ddcb9ba2e12c27 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Fri, 26 Jul 2024 08:05:22 +0800 Subject: [PATCH 05/25] 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 | 319 +++++++++--------- lib/fragments/config.dart | 33 +- lib/fragments/connections.dart | 20 +- .../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 | 209 +++++++----- 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, 1760 insertions(+), 1198 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 ca5d76c04..d2383e2a9 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 000000000..5ae2f0622 --- /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 9da48ceb9..237e3df77 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 ef81b2349..809019dcc 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 000000000..798f29681 --- /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 0292a65f1..689946f7a 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 53ea93f87..cb4e9ee07 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 9fa0de257..5d3a96723 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 a33af65ce..58a3252bf 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 fe1076bce..e6d6c6b57 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 40a2f4e68..c71a4ab5a 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"` +} + +var state State -//export getAndroidProps -func getAndroidProps() *C.char { - data, err := json.Marshal(androidProps) +//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 e25331888..9a24da900 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 3ff7828d5..895241a54 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 7a655e983..f7c37781e 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 f46946e88..a2d2e1d66 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 5ae7b20f9..857be5cc7 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 8eb1c7ca6..9883d54d7 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 1f402a1be..4054d6a5f 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 14f6f784f..cddf298d1 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 fbdbc9908..f997e3cc6 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -53,26 +53,26 @@ class _ConnectionsFragmentState extends State { commonScaffoldState?.actions = [ IconButton( onPressed: () { - clashCore.closeConnections(); - connectionsNotifier.value = connectionsNotifier.value.copyWith( - connections: clashCore.getConnections(), + showSearch( + context: context, + delegate: ConnectionsSearchDelegate( + state: connectionsNotifier.value, + ), ); }, - icon: const Icon(Icons.delete_sweep_outlined), + icon: const Icon(Icons.search), ), const SizedBox( width: 8, ), IconButton( onPressed: () { - showSearch( - context: context, - delegate: ConnectionsSearchDelegate( - state: connectionsNotifier.value, - ), + clashCore.closeConnections(); + connectionsNotifier.value = connectionsNotifier.value.copyWith( + connections: clashCore.getConnections(), ); }, - icon: const Icon(Icons.search), + icon: const Icon(Icons.delete_sweep_outlined), ), ]; }, diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index b14485ce6..b656045c0 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 1e99efeb6..fe6d25ad9 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 e9f0b6433..5c8d2189e 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: [ - CommonPopupMenuItem( - action: ProfileActions.edit, - label: appLocalizations.edit, - iconData: Icons.edit, - ), - 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, - ), + 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.delete, - label: appLocalizations.delete, - iconData: Icons.delete, + action: ProfileActions.update, + label: appLocalizations.update, + iconData: Icons.sync, ), - ], - 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 8adc8a3d7..000000000 --- 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 d58971d2f..3023817ec 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(), + 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: [ + 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, + ], ), ), - ); - }, + ], ), ), - 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(), - ), + ), + 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(), ), ), - delayText, - ], + ); + }, + 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 31453b5a5..629d0dd1a 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 1ce6955c6..3c8c5ff94 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 f9fd68cad..d8a26b64c 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 c3c0be0ea..0bbda9d09 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 ccadef465..9d78ad8cf 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 65c600e1d..e3f295578 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 fbdc730d3..4e1979552 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 8190eb781..63f5e4c01 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 8637f273d..e2560484d 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 609a1add9..081f64929 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 b097b379c..30239fb3c 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 000000000..0c3b97e45 --- /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 de29e2a75..b708a27a6 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 e626e541d..ff6962f34 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 000000000..81f4da5ef --- /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 f963e39fb..d9c81dc4b 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 8b3f0f095..43838359b 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 bd40a83ad..f4ee0dcf5 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 370aa3d5d..9532967f2 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 ab2654a43..e122454de 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 83e38d077..f2e457779 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 e86bf2062..c5fde7c48 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 3e844de49..d55cdfc5e 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 164e26f29..ca212ad22 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 bb7265f46..f64957c48 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 37592c321..0111af9bb 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 000000000..829439d98 --- /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 15e51ff06..a743251a8 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 c2fae4b00..c3f7aa740 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 0dd2c3de6..46e40a0b2 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 73b46a430..99f4c33e4 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 dee4e5200..cc0432f89 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 d546f326c..b132da7dd 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 0b22e1c24..e1ab8a9aa 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 6c35df566..8e734a704 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 03a6feb7b..251cf7696 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' From 8cdaf30de0e4e7d5b844aa92820fc4fda960fc54 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Wed, 31 Jul 2024 21:24:31 +0800 Subject: [PATCH 06/25] Fix linux core build error --- core/Clash.Meta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Clash.Meta b/core/Clash.Meta index 689946f7a..3d773d7fa 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 689946f7a68b2e20ced1b3336f8dba43c3e2afb6 +Subproject commit 3d773d7fa599f30ad28e1ad22e8bf91a2a446f7d From 00a78b5fb4af2fbd10a9fe5f7741f855317f4b1d Mon Sep 17 00:00:00 2001 From: chen08209 Date: Thu, 1 Aug 2024 23:51:00 +0800 Subject: [PATCH 07/25] Fix android tile service issues --- lib/application.dart | 3 +- lib/common/color.dart | 11 +- lib/fragments/profiles/edit_profile.dart | 104 +++++++--------- lib/fragments/profiles/profiles.dart | 135 ++++++++++++--------- lib/fragments/theme.dart | 26 +++- lib/l10n/arb/intl_en.arb | 3 +- lib/l10n/arb/intl_zh_CN.arb | 3 +- lib/l10n/intl/messages_en.dart | 2 + lib/l10n/intl/messages_zh_CN.dart | 1 + lib/l10n/l10n.dart | 10 ++ lib/models/config.dart | 14 +++ lib/models/generated/config.g.dart | 2 + lib/models/generated/selector.freezed.dart | 48 ++++++-- lib/models/selector.dart | 7 +- lib/models/system_color_scheme.dart | 6 +- lib/state.dart | 9 ++ lib/widgets/sheet.dart | 53 ++++---- pubspec.yaml | 4 +- 18 files changed, 268 insertions(+), 173 deletions(-) diff --git a/lib/application.dart b/lib/application.dart index 9a24da900..d3288047a 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -142,6 +142,7 @@ class ApplicationState extends State { locale: config.locale, themeMode: config.themeMode, primaryColor: config.primaryColor, + prueBlack: config.prueBlack, ), builder: (_, state, child) { return DynamicColorBuilder( @@ -180,7 +181,7 @@ class ApplicationState extends State { brightness: Brightness.dark, systemColorSchemes: systemColorSchemes, primaryColor: state.primaryColor, - ), + ).toPrueBlack(state.prueBlack), ), home: child, ); diff --git a/lib/common/color.dart b/lib/common/color.dart index 41e9e4a81..30fe717fe 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -16,4 +16,13 @@ extension ColorExtension on Color { toLittle() { return withOpacity(0.03); } -} \ No newline at end of file +} + +extension ColorSchemeExtension on ColorScheme { + ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack + ? copyWith( + surface: Colors.black, + background: Colors.black, + ) + : this; +} diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index fe6d25ad9..8bcfee727 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -89,8 +89,11 @@ class _EditProfileState extends State { }); } - Future _getFileInfo(path) async { + Future _getFileInfo(path) async { final file = File(path); + if (!await file.exists()) { + return null; + } final lastModified = await file.lastModified(); final size = await file.length(); return FileInfo( @@ -127,59 +130,6 @@ class _EditProfileState extends State { ); } - 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 = [ @@ -250,9 +200,49 @@ class _EditProfileState extends State { ), ), ], - ListItem( - title: Text(appLocalizations.profile), - subtitle: _buildSubtitle(), + ValueListenableBuilder( + valueListenable: fileInfoNotifier, + builder: (_, fileInfo, __) { + return FadeBox( + child: fileInfo == null + ? Container() + : ListItem( + title: Text( + appLocalizations.profile, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + 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, + ), + ], + ), + ], + ), + ), + ); + }, ), ]; return FloatLayout( diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 5c8d2189e..0c70c8d43 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -189,7 +189,7 @@ class _ProfileItemState extends State { ), onTab: () async { await globalState.appController.deleteProfile(widget.profile.id); - if(mounted){ + if (mounted) { Navigator.of(context).pop(); } }, @@ -231,75 +231,90 @@ class _ProfileItemState extends State { ); } + List _buildUserInfo(UserInfo userInfo) { + final use = userInfo.upload + userInfo.download; + final total = userInfo.total; + if(total == 0){ + return []; + } + 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 [ + LinearProgressIndicator( + minHeight: 6, + value: progress, + ), + const SizedBox( + height: 8, + ), + Text( + "$useShow / $totalShow · $expireShow", + style: context.textTheme.labelMedium?.toLight, + ), + const SizedBox( + height: 4, + ), + ]; + } + + List _buildUrlProfileInfo(Profile profile) { + final userInfo = profile.userInfo; + return [ + const SizedBox( + height: 8, + ), + if (userInfo != null) ..._buildUserInfo(userInfo), + Text( + profile.lastUpdateDate?.lastUpdateTimeDesc ?? "", + style: context.textTheme.labelMedium?.toLight, + ), + ]; + } + + List _buildFileProfileInfo(Profile profile) { + return [ + const SizedBox( + height: 8, + ), + Text( + profile.lastUpdateDate?.lastUpdateTimeDesc ?? "", + style: context.textTheme.labelMedium?.toLight, + ), + ]; + } + _buildTitle(Profile profile) { - final textTheme = context.textTheme; return Container( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - profile.label ?? profile.id, - style: textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - profile.lastUpdateDate?.lastUpdateTimeDesc ?? '', - style: textTheme.labelMedium?.toLight, - ), - ], + Text( + profile.label ?? profile.id, + style: context.textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - Builder(builder: (context) { - final userInfo = profile.userInfo ?? const UserInfo(); - 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, + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...switch (profile.type) { + ProfileType.file => _buildFileProfileInfo( + profile, ), - child: LinearProgressIndicator( - minHeight: 6, - value: progress, + ProfileType.url => _buildUrlProfileInfo( + profile, ), - ), - Text( - "$useShow / $totalShow", - style: textTheme.labelMedium?.toLight, - ), - const SizedBox( - height: 2, - ), - Row( - children: [ - Text( - expireShow, - style: textTheme.labelMedium?.toLight, - ), - ], - ) - ], - ); - }), + }, + ], + ), ], ), ); diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index 763c4def4..d6b1e4592 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -26,9 +26,7 @@ class ThemeFragment extends StatelessWidget { final previewCard = Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: CommonCard( - onPressed: (){ - - }, + onPressed: () {}, info: Info( label: appLocalizations.preview, iconData: Icons.looks, @@ -87,7 +85,6 @@ class ThemeColorsBox extends StatefulWidget { } class _ThemeColorsBoxState extends State { - Widget _themeModeCheckBox({ bool? isSelected, required ThemeModeItem themeModeItem, @@ -229,6 +226,27 @@ class _ThemeColorsBoxState extends State { ), ), ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Selector( + selector: (_, config) => config.prueBlack, + builder: (_, value, ___) { + return ListItem.switchItem( + leading: Icon( + Icons.contrast, + color: context.colorScheme.primary, + ), + title: Text(appLocalizations.prueBlackMode), + delegate: SwitchDelegate( + value: value, + onChanged: (value){ + globalState.appController.config.prueBlack = value; + } + ), + ); + }, + ), + ) ], ); } diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 9d78ad8cf..7a59f2012 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -219,5 +219,6 @@ "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?" + "deleteProfileTip": "Sure you want to delete the current profile?", + "prueBlackMode": "Prue black mode" } \ 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 e3f295578..f8e600921 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -219,5 +219,6 @@ "autoCloseConnectionsDesc": "切换节点后自动关闭连接", "onlyStatisticsProxy": "仅统计代理", "onlyStatisticsProxyDesc": "开启后,将只统计代理流量", - "deleteProfileTip": "确定要删除当前配置吗?" + "deleteProfileTip": "确定要删除当前配置吗?", + "prueBlackMode": "纯黑模式" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 4e1979552..a5f56b791 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -257,6 +257,8 @@ class MessageLookup extends MessageLookupByLibrary { "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage( "Set the Clash listening port"), + "prueBlackMode": + MessageLookupByLibrary.simpleMessage("Prue black mode"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Scan QR code to obtain profile"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 63f5e4c01..6cb107a18 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -208,6 +208,7 @@ class MessageLookup extends MessageLookupByLibrary { "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), + "prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index e2560484d..f4c99dccd 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2259,6 +2259,16 @@ class AppLocalizations { args: [], ); } + + /// `Prue black mode` + String get prueBlackMode { + return Intl.message( + 'Prue black mode', + name: 'prueBlackMode', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/models/config.dart b/lib/models/config.dart index 30239fb3c..5384b67a1 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -82,6 +82,7 @@ class Config extends ChangeNotifier { String _testUrl; WindowProps _windowProps; bool _onlyProxy; + bool _prueBlack; Config() : _profiles = [], @@ -107,6 +108,7 @@ class Config extends ChangeNotifier { _windowProps = defaultWindowProps, _proxiesType = ProxiesType.tab, _proxiesColumns = 2, + _prueBlack = false, _onlyProxy = false; deleteProfileById(String id) { @@ -423,6 +425,17 @@ class Config extends ChangeNotifier { } } + @JsonKey(defaultValue: false) + bool get prueBlack { + return _prueBlack; + } + + set prueBlack(bool value) { + if (_prueBlack != value) { + _prueBlack = value; + notifyListeners(); + } + } @JsonKey(defaultValue: false) bool get isCloseConnections { @@ -530,6 +543,7 @@ class Config extends ChangeNotifier { _accessControl = config._accessControl; _isAnimateToPage = config._isAnimateToPage; _autoCheckUpdate = config._autoCheckUpdate; + _prueBlack = config._prueBlack; _testUrl = config._testUrl; _isExclude = config._isExclude; _windowProps = config._windowProps; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index ff6962f34..db7095823 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -36,6 +36,7 @@ Config _$ConfigFromJson(Map json) => Config() ..allowBypass = json['allowBypass'] as bool? ?? true ..systemProxy = json['systemProxy'] as bool? ?? false ..onlyProxy = json['onlyProxy'] as bool? ?? false + ..prueBlack = json['prueBlack'] as bool? ?? false ..isCloseConnections = json['isCloseConnections'] as bool? ?? false ..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'], unknownValue: ProxiesType.tab) ?? @@ -71,6 +72,7 @@ Map _$ConfigToJson(Config instance) => { 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, 'onlyProxy': instance.onlyProxy, + 'prueBlack': instance.prueBlack, 'isCloseConnections': instance.isCloseConnections, 'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!, 'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!, diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index f4ee0dcf5..3e585d924 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -633,6 +633,7 @@ mixin _$ApplicationSelectorState { String? get locale => throw _privateConstructorUsedError; ThemeMode? get themeMode => throw _privateConstructorUsedError; int? get primaryColor => throw _privateConstructorUsedError; + bool get prueBlack => throw _privateConstructorUsedError; @JsonKey(ignore: true) $ApplicationSelectorStateCopyWith get copyWith => @@ -645,7 +646,11 @@ abstract class $ApplicationSelectorStateCopyWith<$Res> { $Res Function(ApplicationSelectorState) then) = _$ApplicationSelectorStateCopyWithImpl<$Res, ApplicationSelectorState>; @useResult - $Res call({String? locale, ThemeMode? themeMode, int? primaryColor}); + $Res call( + {String? locale, + ThemeMode? themeMode, + int? primaryColor, + bool prueBlack}); } /// @nodoc @@ -665,6 +670,7 @@ class _$ApplicationSelectorStateCopyWithImpl<$Res, Object? locale = freezed, Object? themeMode = freezed, Object? primaryColor = freezed, + Object? prueBlack = null, }) { return _then(_value.copyWith( locale: freezed == locale @@ -679,6 +685,10 @@ class _$ApplicationSelectorStateCopyWithImpl<$Res, ? _value.primaryColor : primaryColor // ignore: cast_nullable_to_non_nullable as int?, + prueBlack: null == prueBlack + ? _value.prueBlack + : prueBlack // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -692,7 +702,11 @@ abstract class _$$ApplicationSelectorStateImplCopyWith<$Res> __$$ApplicationSelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({String? locale, ThemeMode? themeMode, int? primaryColor}); + $Res call( + {String? locale, + ThemeMode? themeMode, + int? primaryColor, + bool prueBlack}); } /// @nodoc @@ -711,6 +725,7 @@ class __$$ApplicationSelectorStateImplCopyWithImpl<$Res> Object? locale = freezed, Object? themeMode = freezed, Object? primaryColor = freezed, + Object? prueBlack = null, }) { return _then(_$ApplicationSelectorStateImpl( locale: freezed == locale @@ -725,6 +740,10 @@ class __$$ApplicationSelectorStateImplCopyWithImpl<$Res> ? _value.primaryColor : primaryColor // ignore: cast_nullable_to_non_nullable as int?, + prueBlack: null == prueBlack + ? _value.prueBlack + : prueBlack // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -733,7 +752,10 @@ class __$$ApplicationSelectorStateImplCopyWithImpl<$Res> class _$ApplicationSelectorStateImpl implements _ApplicationSelectorState { const _$ApplicationSelectorStateImpl( - {this.locale, this.themeMode, this.primaryColor}); + {required this.locale, + required this.themeMode, + required this.primaryColor, + required this.prueBlack}); @override final String? locale; @@ -741,10 +763,12 @@ class _$ApplicationSelectorStateImpl implements _ApplicationSelectorState { final ThemeMode? themeMode; @override final int? primaryColor; + @override + final bool prueBlack; @override String toString() { - return 'ApplicationSelectorState(locale: $locale, themeMode: $themeMode, primaryColor: $primaryColor)'; + return 'ApplicationSelectorState(locale: $locale, themeMode: $themeMode, primaryColor: $primaryColor, prueBlack: $prueBlack)'; } @override @@ -756,11 +780,14 @@ class _$ApplicationSelectorStateImpl implements _ApplicationSelectorState { (identical(other.themeMode, themeMode) || other.themeMode == themeMode) && (identical(other.primaryColor, primaryColor) || - other.primaryColor == primaryColor)); + other.primaryColor == primaryColor) && + (identical(other.prueBlack, prueBlack) || + other.prueBlack == prueBlack)); } @override - int get hashCode => Object.hash(runtimeType, locale, themeMode, primaryColor); + int get hashCode => + Object.hash(runtimeType, locale, themeMode, primaryColor, prueBlack); @JsonKey(ignore: true) @override @@ -772,9 +799,10 @@ class _$ApplicationSelectorStateImpl implements _ApplicationSelectorState { abstract class _ApplicationSelectorState implements ApplicationSelectorState { const factory _ApplicationSelectorState( - {final String? locale, - final ThemeMode? themeMode, - final int? primaryColor}) = _$ApplicationSelectorStateImpl; + {required final String? locale, + required final ThemeMode? themeMode, + required final int? primaryColor, + required final bool prueBlack}) = _$ApplicationSelectorStateImpl; @override String? get locale; @@ -783,6 +811,8 @@ abstract class _ApplicationSelectorState implements ApplicationSelectorState { @override int? get primaryColor; @override + bool get prueBlack; + @override @JsonKey(ignore: true) _$$ApplicationSelectorStateImplCopyWith<_$ApplicationSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/selector.dart b/lib/models/selector.dart index f2e457779..addac1248 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -41,9 +41,10 @@ class ProfilesSelectorState with _$ProfilesSelectorState { @freezed class ApplicationSelectorState with _$ApplicationSelectorState { const factory ApplicationSelectorState({ - String? locale, - ThemeMode? themeMode, - int? primaryColor, + required String? locale, + required ThemeMode? themeMode, + required int? primaryColor, + required bool prueBlack, }) = _ApplicationSelectorState; } diff --git a/lib/models/system_color_scheme.dart b/lib/models/system_color_scheme.dart index 64ab2beeb..549ceaea2 100644 --- a/lib/models/system_color_scheme.dart +++ b/lib/models/system_color_scheme.dart @@ -12,15 +12,15 @@ class SystemColorSchemes { }); getSystemColorSchemeForBrightness(Brightness? brightness) { - if (brightness != null && brightness == Brightness.dark) { + if (brightness == Brightness.dark) { return darkColorScheme != null ? ColorScheme.fromSeed( seedColor: darkColorScheme!.primary, - brightness: brightness, + brightness: Brightness.dark, ) : ColorScheme.fromSeed( seedColor: defaultPrimaryColor, - brightness: brightness, + brightness: Brightness.dark, ); } return lightColorScheme != null diff --git a/lib/state.dart b/lib/state.dart index f64957c48..6bf9fbc73 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -111,6 +111,15 @@ class GlobalState { config: config, clashConfig: clashConfig, ); + clashCore.setState( + CoreState( + accessControl: config.isAccessControl ? config.accessControl : null, + allowBypass: config.allowBypass, + systemProxy: config.systemProxy, + mixedPort: clashConfig.mixedPort, + onlyProxy: config.onlyProxy, + ), + ); } updateCoreVersionInfo(appState); } diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index cc0432f89..d21b99ede 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -1,14 +1,11 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'side_sheet.dart'; -showExtendPage( - BuildContext context, { +showExtendPage(BuildContext context, { required Widget body, required String title, double? extendPageWidth, @@ -20,35 +17,31 @@ showExtendPage( key: globalKey, child: body, ); + final isMobile = globalState.appController.appState.viewMode == + ViewMode.mobile; navigator.push( ModalSideSheetRoute( modalBarrierColor: Colors.black38, - builder: (context) => Selector( - selector: (_, appState) => appState.viewWidth, - builder: (_, viewWidth, __) { - final isMobile = - globalState.appController.appState.viewMode == ViewMode.mobile; - final commonScaffold = CommonScaffold( - automaticallyImplyLeading: isMobile ? true : false, - actions: isMobile - ? null - : [ - const SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: CloseButton(), - ), - ], - title: title, - body: uniqueBody, - ); - return AnimatedContainer( - duration: kThemeAnimationDuration, - width: isMobile ? viewWidth : extendPageWidth ?? 300, - child: commonScaffold, - ); - }, - ), + builder: (context) { + final commonScaffold = CommonScaffold( + automaticallyImplyLeading: isMobile ? true : false, + actions: isMobile + ? null + : [ + const SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: CloseButton(), + ), + ], + title: title, + body: uniqueBody, + ); + return SizedBox( + width: isMobile ? context.width : extendPageWidth ?? 300, + child: commonScaffold, + ); + }, constraints: const BoxConstraints(), filter: filter, ), diff --git a/pubspec.yaml b/pubspec.yaml index 251cf7696..1cd0df386 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.49+202407311 +version: 0.8.50+202408011 environment: sdk: '>=3.1.0 <4.0.0' @@ -38,8 +38,6 @@ dependencies: webdav_client: ^1.2.2 dio: ^5.4.3+1 country_flags: ^2.2.0 - re_editor: ^0.3.0 - re_highlight: ^0.0.3 win32: ^5.5.1 ffi: ^2.1.2 material_color_utilities: ^0.8.0 From 7fc54c5295007152c1fe7e251109ef98c4d52104 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 4 Aug 2024 08:21:14 +0800 Subject: [PATCH 08/25] Optimize provider page Optimize delay test Support local backup and recovery --- core/Clash.Meta | 2 +- core/common.go | 163 ++++++--- core/hub.go | 176 ++++++---- core/state.go | 5 +- lib/clash/core.dart | 47 +-- lib/clash/generated/clash_ffi.dart | 39 +-- lib/common/archive.dart | 28 ++ lib/common/color.dart | 8 + lib/common/constant.dart | 2 +- lib/common/dav_client.dart | 62 +--- lib/common/navigation.dart | 2 +- lib/common/other.dart | 5 + lib/common/path.dart | 9 + lib/common/picker.dart | 16 +- lib/common/window.dart | 1 + lib/controller.dart | 99 ++++-- lib/fragments/access.dart | 105 +++--- lib/fragments/backup_and_recovery.dart | 364 ++++++++++++--------- lib/fragments/config.dart | 105 +++++- lib/fragments/profiles/edit_profile.dart | 30 +- lib/fragments/profiles/profiles.dart | 315 ++++++++---------- lib/fragments/profiles/view_profile.dart | 232 +++++++++++++ lib/fragments/proxies/common.dart | 10 +- lib/fragments/proxies/providers.dart | 187 +++++++++++ lib/fragments/proxies/proxies.dart | 46 ++- lib/fragments/resources.dart | 218 +----------- lib/l10n/arb/intl_en.arb | 15 +- lib/l10n/arb/intl_zh_CN.arb | 15 +- lib/l10n/intl/messages_en.dart | 22 +- lib/l10n/intl/messages_zh_CN.dart | 15 +- lib/l10n/l10n.dart | 114 +++++-- lib/models/app.dart | 30 ++ lib/models/clash_config.dart | 12 + lib/models/config.dart | 1 + lib/models/ffi.dart | 11 +- lib/models/generated/clash_config.g.dart | 2 + lib/models/generated/config.freezed.dart | 27 +- lib/models/generated/config.g.dart | 2 + lib/models/generated/ffi.freezed.dart | 116 +++++-- lib/models/generated/ffi.g.dart | 10 +- lib/models/generated/profile.freezed.dart | 42 ++- lib/models/generated/selector.freezed.dart | 148 ++++----- lib/models/profile.dart | 9 +- lib/models/proxy.dart | 2 +- lib/models/selector.dart | 12 +- lib/state.dart | 12 +- lib/widgets/builder.dart | 29 +- lib/widgets/card.dart | 61 ++-- lib/widgets/clash_container.dart | 42 ++- lib/widgets/sheet.dart | 25 +- lib/widgets/window_container.dart | 18 +- pubspec.lock | 34 +- pubspec.yaml | 5 +- 53 files changed, 1986 insertions(+), 1121 deletions(-) create mode 100644 lib/common/archive.dart create mode 100644 lib/fragments/profiles/view_profile.dart create mode 100644 lib/fragments/proxies/providers.dart diff --git a/core/Clash.Meta b/core/Clash.Meta index 3d773d7fa..fffdf8449 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 3d773d7fa599f30ad28e1ad22e8bf91a2a446f7d +Subproject commit fffdf84493f054423b23e6883bcc2cdcfe877439 diff --git a/core/common.go b/core/common.go index cb4e9ee07..45056e0fc 100644 --- a/core/common.go +++ b/core/common.go @@ -2,19 +2,23 @@ package main import "C" import ( + "context" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outboundgroup" - ap "github.com/metacubex/mihomo/adapter/provider" + "github.com/metacubex/mihomo/adapter/provider" + "github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" + cp "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/route" "github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/log" + rp "github.com/metacubex/mihomo/rules/provider" "github.com/metacubex/mihomo/tunnel" "os" "os/exec" @@ -26,40 +30,40 @@ import ( "time" ) -type healthCheckSchema struct { - Enable bool `provider:"enable"` - URL string `provider:"url"` - Interval int `provider:"interval"` - TestTimeout int `provider:"timeout,omitempty"` - Lazy bool `provider:"lazy,omitempty"` - ExpectedStatus string `provider:"expected-status,omitempty"` -} - -type proxyProviderSchema struct { - Type string `provider:"type"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Proxy string `provider:"proxy,omitempty"` - Interval int `provider:"interval,omitempty"` - Filter string `provider:"filter,omitempty"` - ExcludeFilter string `provider:"exclude-filter,omitempty"` - ExcludeType string `provider:"exclude-type,omitempty"` - DialerProxy string `provider:"dialer-proxy,omitempty"` - - HealthCheck healthCheckSchema `provider:"health-check,omitempty"` - Override ap.OverrideSchema `provider:"override,omitempty"` - Header map[string][]string `provider:"header,omitempty"` -} - -type ruleProviderSchema struct { - Type string `provider:"type"` - Behavior string `provider:"behavior"` - Path string `provider:"path,omitempty"` - URL string `provider:"url,omitempty"` - Proxy string `provider:"proxy,omitempty"` - Format string `provider:"format,omitempty"` - Interval int `provider:"interval,omitempty"` -} +//type healthCheckSchema struct { +// Enable bool `provider:"enable"` +// URL string `provider:"url"` +// Interval int `provider:"interval"` +// TestTimeout int `provider:"timeout,omitempty"` +// Lazy bool `provider:"lazy,omitempty"` +// ExpectedStatus string `provider:"expected-status,omitempty"` +//} + +//type proxyProviderSchema struct { +// Type string `provider:"type"` +// Path string `provider:"path,omitempty"` +// URL string `provider:"url,omitempty"` +// Proxy string `provider:"proxy,omitempty"` +// Interval int `provider:"interval,omitempty"` +// Filter string `provider:"filter,omitempty"` +// ExcludeFilter string `provider:"exclude-filter,omitempty"` +// ExcludeType string `provider:"exclude-type,omitempty"` +// DialerProxy string `provider:"dialer-proxy,omitempty"` +// +// HealthCheck healthCheckSchema `provider:"health-check,omitempty"` +// Override ap.OverrideSchema `provider:"override,omitempty"` +// Header map[string][]string `provider:"header,omitempty"` +//} +// +//type ruleProviderSchema struct { +// Type string `provider:"type"` +// Behavior string `provider:"behavior"` +// Path string `provider:"path,omitempty"` +// URL string `provider:"url,omitempty"` +// Proxy string `provider:"proxy,omitempty"` +// Format string `provider:"format,omitempty"` +// Interval int `provider:"interval,omitempty"` +//} type ConfigExtendedParams struct { IsPatch bool `json:"is-patch"` @@ -69,9 +73,9 @@ type ConfigExtendedParams struct { } type GenerateConfigParams struct { - ProfilePath *string `json:"profile-path"` - Config config.RawConfig `json:"config" ` - Params ConfigExtendedParams `json:"params"` + ProfileId string `json:"profile-id"` + Config config.RawConfig `json:"config" ` + Params ConfigExtendedParams `json:"params"` } type ChangeProxyParams struct { @@ -93,9 +97,13 @@ type ExternalProvider struct { Name string `json:"name"` Type string `json:"type"` VehicleType string `json:"vehicle-type"` + Count int `json:"count"` + Path string `json:"path"` UpdateAt time.Time `json:"update-at"` } +var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) + func restartExecutable(execPath string) { var err error executor.Shutdown() @@ -145,26 +153,76 @@ func removeFile(path string) error { return nil } -func getRawConfigWithPath(path *string) *config.RawConfig { - if path == nil { +func getProfilePath(id string) string { + return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml") +} + +func getProfileProvidersPath(id string) string { + return filepath.Join(constant.Path.HomeDir(), "providers", id) +} + +func getRawConfigWithId(id string) *config.RawConfig { + path := getProfilePath(id) + bytes, err := readFile(path) + if err != nil { + log.Errorln("profile is not exist") return config.DefaultRawConfig() - } else { - bytes, err := readFile(*path) - if err != nil { - log.Errorln("getProfile readFile error %v", err) - return config.DefaultRawConfig() + } + prof, err := config.UnmarshalRawConfig(bytes) + if err != nil { + log.Errorln("unmarshalRawConfig error %v", err) + return config.DefaultRawConfig() + } + for _, mapping := range prof.ProxyProvider { + value, exist := mapping["path"].(string) + if !exist { + continue } - prof, err := config.UnmarshalRawConfig(bytes) - if err != nil { - log.Errorln("getProfile UnmarshalRawConfig error %v", err) - return config.DefaultRawConfig() + mapping["path"] = filepath.Join(getProfileProvidersPath(id), value) + } + for _, mapping := range prof.RuleProvider { + value, exist := mapping["path"].(string) + if !exist { + continue + } + mapping["path"] = filepath.Join(getProfileProvidersPath(id), value) + } + return prof +} + +func getExternalProvidersRaw() map[string]ExternalProvider { + externalProviders := make(map[string]ExternalProvider) + for n, p := range tunnel.Providers() { + if p.VehicleType() != cp.Compatible { + p := p.(*provider.ProxySetProvider) + externalProviders[n] = ExternalProvider{ + Name: n, + Type: p.Type().String(), + VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), + UpdateAt: p.UpdatedAt, + } + } + } + for n, p := range tunnel.RuleProviders() { + if p.VehicleType() != cp.Compatible { + p := p.(*rp.RuleSetProvider) + externalProviders[n] = ExternalProvider{ + Name: n, + Type: p.Type().String(), + VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), + UpdateAt: p.UpdatedAt, + } } - return prof } + return externalProviders } -func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig { - prof := getRawConfigWithPath(profilePath) +func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig { + prof := getRawConfigWithId(profileId) overwriteConfig(prof, cfg) return prof } @@ -327,6 +385,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi targetConfig.LogLevel = patchConfig.LogLevel targetConfig.Port = 0 targetConfig.SocksPort = 0 + targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval targetConfig.MixedPort = patchConfig.MixedPort targetConfig.FindProcessMode = patchConfig.FindProcessMode targetConfig.AllowLan = patchConfig.AllowLan diff --git a/core/hub.go b/core/hub.go index e6d6c6b57..355b0d776 100644 --- a/core/hub.go +++ b/core/hub.go @@ -11,7 +11,6 @@ import ( "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/provider" - "github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" @@ -35,8 +34,6 @@ var configParams = ConfigExtendedParams{} var isInit = false -var currentProfileName = "" - //export initClash func initClash(homeDirStr *C.char) bool { if !isInit { @@ -75,16 +72,6 @@ func forceGc() { }() } -//export setCurrentProfileName -func setCurrentProfileName(s *C.char) { - currentProfileName = C.GoString(s) -} - -//export getCurrentProfileName -func getCurrentProfileName() *C.char { - return C.CString(currentProfileName) -} - //export validateConfig func validateConfig(s *C.char, port C.longlong) { i := int64(port) @@ -111,7 +98,7 @@ func updateConfig(s *C.char, port C.longlong) { return } configParams = params.Params - prof := decorationConfig(params.ProfilePath, params.Config) + prof := decorationConfig(params.ProfileId, params.Config) currentConfig = prof err = applyConfig() if err != nil { @@ -124,34 +111,10 @@ func updateConfig(s *C.char, port C.longlong) { //export clearEffect func clearEffect(s *C.char) { - path := C.GoString(s) + id := C.GoString(s) go func() { - rawCfg := getRawConfigWithPath(&path) - for _, mapping := range rawCfg.RuleProvider { - schema := &ruleProviderSchema{} - decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) - if err := decoder.Decode(mapping, schema); err != nil { - return - } - if schema.Type == "http" { - _ = removeFile(constant.Path.Resolve(schema.Path)) - } - } - for _, mapping := range rawCfg.ProxyProvider { - schema := &proxyProviderSchema{ - HealthCheck: healthCheckSchema{ - Lazy: true, - }, - } - decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) - if err := decoder.Decode(mapping, schema); err != nil { - return - } - if schema.Type == "http" { - _ = removeFile(constant.Path.Resolve(schema.Path)) - } - } - _ = removeFile(path) + _ = removeFile(getProfilePath(id)) + _ = removeFile(getProfileProvidersPath(id)) }() } @@ -184,10 +147,13 @@ func changeProxy(s *C.char) { if !ok { return } - - err = selector.Set(proxyName) + if proxyName == "" { + selector.ForceSet(proxyName) + } else { + err = selector.Set(proxyName) + } if err == nil { - log.Infoln("[Selector] %s selected %s", groupName, proxyName) + log.Infoln("[SelectAble] %s selected %s", groupName, proxyName) } } @@ -230,16 +196,16 @@ func resetTraffic() { func asyncTestDelay(s *C.char, port C.longlong) { i := int64(port) paramsString := C.GoString(s) - go func() { + b.Go(paramsString, func() (bool, error) { var params = &TestDelayParams{} err := json.Unmarshal([]byte(paramsString), params) if err != nil { - return + return false, nil } expectedStatus, err := utils.NewUnsignedRanges[uint16]("") if err != nil { - return + return false, nil } ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout)) @@ -256,7 +222,7 @@ func asyncTestDelay(s *C.char, port C.longlong) { delayData.Value = -1 data, _ := json.Marshal(delayData) bridge.SendToPort(i, string(data)) - return + return false, nil } delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) @@ -264,14 +230,14 @@ func asyncTestDelay(s *C.char, port C.longlong) { delayData.Value = -1 data, _ := json.Marshal(delayData) bridge.SendToPort(i, string(data)) - return + return false, nil } delayData.Value = int32(delay) data, _ := json.Marshal(delayData) bridge.SendToPort(i, string(data)) - return - }() + return false, nil + }) } //export getVersionInfo @@ -345,28 +311,31 @@ func getProvider(name *C.char) *C.char { //export getExternalProviders func getExternalProviders() *C.char { - externalProviders := make([]ExternalProvider, 0) - providers := tunnel.Providers() - for n, p := range providers { + externalProviders := make(map[string]ExternalProvider) + for n, p := range tunnel.Providers() { if p.VehicleType() != cp.Compatible { p := p.(*provider.ProxySetProvider) - externalProviders = append(externalProviders, ExternalProvider{ + externalProviders[n] = ExternalProvider{ Name: n, Type: p.Type().String(), VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), UpdateAt: p.UpdatedAt, - }) + } } } for n, p := range tunnel.RuleProviders() { if p.VehicleType() != cp.Compatible { p := p.(*rp.RuleSetProvider) - externalProviders = append(externalProviders, ExternalProvider{ + externalProviders[n] = ExternalProvider{ Name: n, Type: p.Type().String(), VehicleType: p.VehicleType().String(), + Count: p.Count(), + Path: p.Vehicle().Path(), UpdateAt: p.UpdatedAt, - }) + } } } data, err := json.Marshal(externalProviders) @@ -376,6 +345,21 @@ func getExternalProviders() *C.char { return C.CString(string(data)) } +//export getExternalProvider +func getExternalProvider(name *C.char) *C.char { + externalProviderName := C.GoString(name) + externalProviders := getExternalProvidersRaw() + externalProvider, exist := externalProviders[externalProviderName] + if !exist { + return C.CString("") + } + data, err := json.Marshal(externalProvider) + if err != nil { + return C.CString("") + } + return C.CString(string(data)) +} + //export updateExternalProvider func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) { i := int64(port) @@ -385,14 +369,24 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l switch providerTypeString { case "Proxy": providers := tunnel.Providers() - err := providers[providerNameString].Update() + proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) + if !exist { + bridge.SendToPort(i, "proxy provider is not exist") + return + } + err := proxyProvider.Update() if err != nil { bridge.SendToPort(i, err.Error()) return } case "Rule": providers := tunnel.RuleProviders() - err := providers[providerNameString].Update() + ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) + if !exist { + bridge.SendToPort(i, "rule provider is not exist") + return + } + err := ruleProvider.Update() if err != nil { bridge.SendToPort(i, err.Error()) return @@ -426,6 +420,66 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l }() } +//func sideLoadExternalProvider(providerName *C.char, providerType *C.char, data *C.char, port C.longlong) { +// i := int64(port) +// bytes := []byte(C.GoString(data)) +// providerNameString := C.GoString(providerName) +// providerTypeString := C.GoString(providerType) +// go func() { +// switch providerTypeString { +// case "Proxy": +// providers := tunnel.Providers() +// proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) +// if exist { +// bridge.SendToPort(i, "proxy provider is not exist") +// return +// } +// err := proxyProvider.Update() +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "Rule": +// providers := tunnel.RuleProviders() +// ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) +// if exist { +// bridge.SendToPort(i, "proxy provider is not exist") +// return +// } +// err := ruleProvider.Update() +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "MMDB": +// err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "ASN": +// err := updater.UpdateASN(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "GeoIp": +// err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// case "GeoSite": +// err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString)) +// if err != nil { +// bridge.SendToPort(i, err.Error()) +// return +// } +// } +// bridge.SendToPort(i, "") +// }() +//} + //export initNativeApiBridge func initNativeApiBridge(api unsafe.Pointer) { bridge.InitDartApi(api) @@ -463,7 +517,7 @@ func init() { Data: c, }) } - executor.DefaultProxyProviderLoadedHook = func(providerName string) { + executor.DefaultProviderLoadedHook = func(providerName string) { SendMessage(Message{ Type: LoadedMessage, Data: providerName, diff --git a/core/state.go b/core/state.go index c71a4ab5a..3034a92f2 100644 --- a/core/state.go +++ b/core/state.go @@ -21,8 +21,9 @@ type AndroidProps struct { type State struct { AndroidProps - MixedPort int `json:"mixedPort"` - OnlyProxy bool `json:"onlyProxy"` + CurrentProfileName string `json:"currentProfileName"` + MixedPort int `json:"mixedPort"` + OnlyProxy bool `json:"onlyProxy"` } var state State diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 895241a54..f518d46f4 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -100,22 +100,6 @@ class ClashCore { ); } - setProfileName(String profileName) { - final profileNameChar = profileName.toNativeUtf8().cast(); - clashFFI.setCurrentProfileName( - profileNameChar, - ); - malloc.free(profileNameChar); - } - - getProfileName() { - final currentProfileNameRaw = clashFFI.getCurrentProfileName(); - final currentProfileName = - currentProfileNameRaw.cast().toDartString(); - clashFFI.freeCString(currentProfileNameRaw); - return currentProfileName; - } - Future> getProxiesGroups() { final proxiesRaw = clashFFI.getProxies(); final proxiesRawString = proxiesRaw.cast().toDartString(); @@ -156,7 +140,8 @@ class ClashCore { clashFFI.freeCString(externalProvidersRaw); return Isolate.run>(() { final externalProviders = - (json.decode(externalProvidersRawString) as List) + (json.decode(externalProvidersRawString) as Map) + .values .map( (item) => ExternalProvider.fromJson(item), ) @@ -165,6 +150,18 @@ class ClashCore { }); } + ExternalProvider getExternalProvider(String externalProviderName) { + final externalProviderNameChar = + externalProviderName.toNativeUtf8().cast(); + final externalProviderRaw = + clashFFI.getExternalProvider(externalProviderNameChar); + malloc.free(externalProviderNameChar); + final externalProviderRawString = + externalProviderRaw.cast().toDartString(); + clashFFI.freeCString(externalProviderRaw); + return ExternalProvider.fromJson(json.decode(externalProviderRawString)); + } + Future updateExternalProvider({ required String providerName, required String providerType, @@ -216,21 +213,13 @@ class ClashCore { receiver.sendPort.nativePort, ); malloc.free(delayParamsChar); - Future.delayed(httpTimeoutDuration + moreDuration, () { - receiver.close(); - if (!completer.isCompleted) { - completer.complete( - Delay(name: proxyName, value: -1), - ); - } - }); return completer.future; } - clearEffect(String path) { - final pathChar = path.toNativeUtf8().cast(); - clashFFI.clearEffect(pathChar); - malloc.free(pathChar); + clearEffect(String profileId) { + final profileIdChar = profileId.toNativeUtf8().cast(); + clashFFI.clearEffect(profileIdChar); + malloc.free(profileIdChar); } VersionInfo getVersionInfo() { diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index f7c37781e..e081ea55a 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -5190,30 +5190,6 @@ class ClashFFI { _lookup>('forceGc'); late final _forceGc = _forceGcPtr.asFunction(); - void setCurrentProfileName( - ffi.Pointer s, - ) { - return _setCurrentProfileName( - s, - ); - } - - late final _setCurrentProfileNamePtr = - _lookup)>>( - 'setCurrentProfileName'); - late final _setCurrentProfileName = _setCurrentProfileNamePtr - .asFunction)>(); - - ffi.Pointer getCurrentProfileName() { - return _getCurrentProfileName(); - } - - late final _getCurrentProfileNamePtr = - _lookup Function()>>( - 'getCurrentProfileName'); - late final _getCurrentProfileName = - _getCurrentProfileNamePtr.asFunction Function()>(); - void validateConfig( ffi.Pointer s, int port, @@ -5409,6 +5385,21 @@ class ClashFFI { late final _getExternalProviders = _getExternalProvidersPtr.asFunction Function()>(); + ffi.Pointer getExternalProvider( + ffi.Pointer name, + ) { + return _getExternalProvider( + name, + ); + } + + late final _getExternalProviderPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('getExternalProvider'); + late final _getExternalProvider = _getExternalProviderPtr + .asFunction Function(ffi.Pointer)>(); + void updateExternalProvider( ffi.Pointer providerName, ffi.Pointer providerType, diff --git a/lib/common/archive.dart b/lib/common/archive.dart new file mode 100644 index 000000000..b2c6630be --- /dev/null +++ b/lib/common/archive.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:path/path.dart'; + +extension ArchiveExt on Archive { + addDirectoryToArchive(String dirPath, String parentPath) { + final dir = Directory(dirPath); + final entities = dir.listSync(recursive: false); + for (final entity in entities) { + final relativePath = relative(entity.path, from: parentPath); + if (entity is File) { + final data = entity.readAsBytesSync(); + final archiveFile = ArchiveFile(relativePath, data.length, data); + addFile(archiveFile); + } else if (entity is Directory) { + addDirectoryToArchive(entity.path, parentPath); + } + } + } + + add(String name, T raw) { + final data = json.encode(raw); + addFile( + ArchiveFile(name, data.length, data), + ); + } +} diff --git a/lib/common/color.dart b/lib/common/color.dart index 30fe717fe..bdad4a49b 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -16,6 +16,13 @@ extension ColorExtension on Color { toLittle() { return withOpacity(0.03); } + + Color darken([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(this); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + return hslDark.toColor(); + } } extension ColorSchemeExtension on ColorScheme { @@ -23,6 +30,7 @@ extension ColorSchemeExtension on ColorScheme { ? copyWith( surface: Colors.black, background: Colors.black, + surfaceContainer: surfaceContainer.darken(0.05), ) : this; } diff --git a/lib/common/constant.dart b/lib/common/constant.dart index a2d2e1d66..a79a283a0 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -17,7 +17,7 @@ const mmdbFileName = "geoip.metadb"; const asnFileName = "ASN.mmdb"; const geoIpFileName = "GeoIP.dat"; const geoSiteFileName = "GeoSite.dat"; -final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0; +final double kHeaderHeight = system.isDesktop ? 40 : 0; const GeoXMap defaultGeoXMap = { "mmdb": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", diff --git a/lib/common/dav_client.dart b/lib/common/dav_client.dart index e546739e9..905624391 100644 --- a/lib/common/dav_client.dart +++ b/lib/common/dav_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -43,65 +44,16 @@ class DAVClient { get root => "/$appName"; - get remoteConfig => "$root/$configKey.json"; + get backupFile => "$root/backup.zip"; - get remoteClashConfig => "$root/$clashConfigKey.json"; - - get remoteProfiles => "$root/$profilesDirectoryName"; - - backup() async { - final appController = globalState.appController; - final config = appController.config; - final clashConfig = appController.clashConfig; + backup(Uint8List data) async { await client.mkdir("$root"); - client.write( - remoteConfig, - utf8.encode( - json.encode(config.toJson()), - ), - ); - client.write( - remoteClashConfig, - utf8.encode( - json.encode(clashConfig.toJson()), - ), - ); - await client.remove(remoteProfiles); - for (final profile in config.profiles) { - final path = await appPath.getProfilePath(profile.id); - if (path == null) continue; - await client.writeFromFile( - path, - "$remoteProfiles/${basename(path)}", - ); - } + await client.write("$backupFile", data); return true; } - recovery({required RecoveryOption recoveryOption}) async { - final profiles = await client.readDir(remoteProfiles); - final profilesPath = await appPath.getProfilesPath(); - for (final file in profiles) { - await client.read2File( - "$remoteProfiles/${file.name}", - join( - profilesPath, - file.name, - ), - ); - } - final configRaw = utf8.decode((await client.read(remoteConfig))); - final clashConfigRaw = utf8.decode(await client.read(remoteClashConfig)); - final config = Config.fromJson(json.decode(configRaw)); - final clashConfig = ClashConfig.fromJson(json.decode(clashConfigRaw)); - if(recoveryOption == RecoveryOption.onlyProfiles){ - globalState.appController.config.update(config, RecoveryOption.onlyProfiles); - }else{ - globalState.appController.config.update(config, RecoveryOption.all); - globalState.appController.clashConfig.update(clashConfig); - } - await globalState.appController.applyProfile(); - globalState.appController.savePreferences(); - return true; + Future> recovery() async { + final data = await client.read(backupFile); + return data; } } diff --git a/lib/common/navigation.dart b/lib/common/navigation.dart index 141b94945..98c33f66d 100644 --- a/lib/common/navigation.dart +++ b/lib/common/navigation.dart @@ -44,7 +44,7 @@ class Navigation { modes: [NavigationItemMode.desktop, NavigationItemMode.more], ), const NavigationItem( - icon: Icon(Icons.swap_vert_circle), + icon: Icon(Icons.storage), label: "resources", description: "resourcesDesc", keep: false, diff --git a/lib/common/other.dart b/lib/common/other.dart index 8f2ff76f4..f9746e6cd 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; @@ -208,6 +209,10 @@ class Other { int() => throw UnimplementedError(), }; } + + String getBackupFileName(){ + return "${appName}_backup_${DateTime.now().show}.zip"; + } } final other = Other(); diff --git a/lib/common/path.dart b/lib/common/path.dart index 25bb1eac8..4945d6080 100644 --- a/lib/common/path.dart +++ b/lib/common/path.dart @@ -9,6 +9,7 @@ import 'constant.dart'; class AppPath { static AppPath? _instance; Completer cacheDir = Completer(); + Completer downloadDir = Completer(); // Future _createDesktopCacheDir() async { // final path = join(dirname(Platform.resolvedExecutable), 'cache'); @@ -23,6 +24,9 @@ class AppPath { getApplicationSupportDirectory().then((value) { cacheDir.complete(value); }); + getDownloadsDirectory().then((value) { + downloadDir.complete(value); + }); // if (Platform.isAndroid) { // getApplicationSupportDirectory().then((value) { // cacheDir.complete(value); @@ -39,6 +43,11 @@ class AppPath { return _instance!; } + Future getDownloadDirPath() async { + final directory = await downloadDir.future; + return directory.path; + } + Future getHomeDirPath() async { final directory = await cacheDir.future; return directory.path; diff --git a/lib/common/picker.dart b/lib/common/picker.dart index 417e5675d..fa98382c4 100644 --- a/lib/common/picker.dart +++ b/lib/common/picker.dart @@ -1,22 +1,26 @@ +import 'dart:typed_data'; + import 'package:file_picker/file_picker.dart'; import 'package:fl_clash/common/common.dart'; import 'package:image_picker/image_picker.dart'; class Picker { - Future pickerConfigFile() async { + Future pickerFile() async { final filePickerResult = await FilePicker.platform.pickFiles( withData: true, allowMultiple: false, + initialDirectory: await appPath.getDownloadDirPath(), ); return filePickerResult?.files.first; } - Future pickerGeoDataFile() async { - final filePickerResult = await FilePicker.platform.pickFiles( - withData: true, - allowMultiple: false, + Future saveFile(String fileName,Uint8List bytes) async { + final path = await FilePicker.platform.saveFile( + fileName: fileName, + initialDirectory: await appPath.getDownloadDirPath(), + bytes: bytes, ); - return filePickerResult?.files.first; + return path; } Future pickerConfigQRCode() async { diff --git a/lib/common/window.dart b/lib/common/window.dart index 2e35d6596..c4aa637aa 100644 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -20,6 +20,7 @@ class Window { WindowOptions windowOptions = WindowOptions( size: Size(props.width, props.height), minimumSize: const Size(380, 500), + windowButtonVisibility: false, titleBarStyle: TitleBarStyle.hidden, ); if (props.left != null || props.top != null) { diff --git a/lib/controller.dart b/lib/controller.dart index 9883d54d7..5f86d4fcd 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -1,9 +1,14 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; +import 'package:archive/archive.dart'; +import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -90,9 +95,7 @@ class AppController { deleteProfile(String id) async { config.deleteProfileById(id); - final profilePath = await appPath.getProfilePath(id); - if (profilePath == null) return; - clashCore.clearEffect(profilePath); + clashCore.clearEffect(id); if (config.currentProfileId == id) { if (config.profiles.isNotEmpty) { final updateId = config.profiles.first.id; @@ -104,8 +107,10 @@ class AppController { } Future updateProfile(Profile profile) async { - await profile.update(); - config.setProfile(await profile.update()); + final newProfile = await profile.update(); + config.setProfile( + newProfile.copyWith(isUpdating: false), + ); } Future updateClashConfig({bool isPatch = true}) async { @@ -140,9 +145,6 @@ class AppController { changeProfile(String? value) async { if (value == config.currentProfileId) return; config.currentProfileId = value; - await applyProfile(); - appState.delayMap = {}; - saveConfigPreferences(); } autoUpdateProfiles() async { @@ -294,26 +296,6 @@ class AppController { if (!config.silentLaunch) { window?.show(); } - final commonScaffoldState = globalState.homeScaffoldKey.currentState; - if (commonScaffoldState?.mounted == true) { - await commonScaffoldState?.loadingRun(() async { - await globalState.applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - }, title: appLocalizations.init); - } else { - await globalState.applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - } - await afterInit(); - } - - afterInit() async { await proxyManager.updateStartTime(); if (proxyManager.isStart) { await updateSystemProxy(true); @@ -403,7 +385,7 @@ class AppController { } addProfileFormFile() async { - final platformFile = await globalState.safeRun(picker.pickerConfigFile); + final platformFile = await globalState.safeRun(picker.pickerFile); final bytes = platformFile?.bytes; if (bytes == null) { return null; @@ -480,4 +462,63 @@ class AppController { config.currentSelectedMap[groupName] ?? '') ?? ''; } + + Future> backupData() async { + final homeDirPath = await appPath.getHomeDirPath(); + final profilesPath = await appPath.getProfilesPath(); + final configJson = config.toJson(); + final clashConfigJson = clashConfig.toJson(); + return Isolate.run>(() async { + final archive = Archive(); + archive.add("config.json", configJson); + archive.add("clashConfig.json", clashConfigJson); + await archive.addDirectoryToArchive(profilesPath, homeDirPath); + final zipEncoder = ZipEncoder(); + return zipEncoder.encode(archive) ?? []; + }); + } + + recoveryData( + List data, + RecoveryOption recoveryOption, + ) async { + final archive = await Isolate.run(() { + final zipDecoder = ZipDecoder(); + return zipDecoder.decodeBytes(data); + }); + final homeDirPath = await appPath.getHomeDirPath(); + final configs = + archive.files.where((item) => item.name.endsWith(".json")).toList(); + final profiles = + archive.files.where((item) => !item.name.endsWith(".json")); + final configIndex = + configs.indexWhere((config) => config.name == "config.json"); + final clashConfigIndex = + configs.indexWhere((config) => config.name == "clashConfig.json"); + if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip"; + final configFile = configs[configIndex]; + final clashConfigFile = configs[clashConfigIndex]; + final tempConfig = Config.fromJson( + json.decode( + utf8.decode(configFile.content), + ), + ); + final tempClashConfig = ClashConfig.fromJson( + json.decode( + utf8.decode(clashConfigFile.content), + ), + ); + for (final profile in profiles) { + final filePath = join(homeDirPath, profile.name); + final file = File(filePath); + await file.create(recursive: true); + await file.writeAsBytes(profile.content); + } + if (recoveryOption == RecoveryOption.onlyProfiles) { + config.update(tempConfig, RecoveryOption.onlyProfiles); + } else { + config.update(tempConfig, RecoveryOption.all); + clashConfig.update(tempClashConfig); + } + } } diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 4054d6a5f..6d1b49330 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -23,24 +23,19 @@ class AccessFragment extends StatefulWidget { } class _AccessFragmentState extends State { - final packagesListenable = ValueNotifier>([]); - @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(const Duration(milliseconds: 300), () async { - packagesListenable.value = await app?.getPackages() ?? []; - }); + final appState = globalState.appController.appState; + if (appState.packages.isEmpty) { + Future.delayed(const Duration(milliseconds: 300), () async { + appState.packages = await app?.getPackages() ?? []; + }); + } }); } - @override - void dispose() { - super.dispose(); - packagesListenable.dispose(); - } - Widget _buildAppProxyModePopup() { final items = [ CommonPopupMenuItem( @@ -156,8 +151,8 @@ class _AccessFragmentState extends State { // } Widget _buildPackageList() { - return ValueListenableBuilder( - valueListenable: packagesListenable, + return Selector>( + selector: (_, appState) => appState.packages, builder: (_, packages, ___) { final accessControl = globalState.appController.config.accessControl; final acceptList = accessControl.acceptList; @@ -238,10 +233,10 @@ class _AccessFragmentState extends State { .textTheme .labelLarge ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), + color: Theme.of(context) + .colorScheme + .primary, + ), ), ), const Flexible( @@ -256,10 +251,10 @@ class _AccessFragmentState extends State { .textTheme .labelLarge ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), + color: Theme.of(context) + .colorScheme + .primary, + ), ), ), ], @@ -288,47 +283,43 @@ class _AccessFragmentState extends State { ), 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( + 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( + } else { + config.accessControl = + config.accessControl.copyWith( rejectList: valueList, ); - } + } + }, + ); }, - ); - }, - ), - ), + ), ), ], ), diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index 2d3068814..cb6c2fef0 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -1,25 +1,22 @@ +import 'dart:io'; +import 'dart:typed_data'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/dav_client.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/dav.dart'; import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/text.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class BackupAndRecovery extends StatefulWidget { +class BackupAndRecovery extends StatelessWidget { const BackupAndRecovery({super.key}); - @override - State createState() => _BackupAndRecoveryState(); -} - -class _BackupAndRecoveryState extends State { - DAVClient? _client; - _showAddWebDAV(DAV? dav) async { await globalState.showCommonDialog( child: WebDAVFormDialog( @@ -28,11 +25,64 @@ class _BackupAndRecoveryState extends State { ); } - _backup() async { + _backupOnWebDAV(BuildContext context, DAVClient client) async { + final commonScaffoldState = context.commonScaffoldState; + final res = await commonScaffoldState?.loadingRun( + () async { + final backupData = await globalState.appController.backupData(); + return await client.backup(Uint8List.fromList(backupData)); + }, + title: appLocalizations.backup, + ); + if (res != true) return; + globalState.showMessage( + title: appLocalizations.backup, + message: TextSpan(text: appLocalizations.backupSuccess), + ); + } + + _recoveryOnWebDAV( + BuildContext context, + DAVClient client, + RecoveryOption recoveryOption, + ) async { + final commonScaffoldState = context.commonScaffoldState; + final res = await commonScaffoldState?.loadingRun( + () async { + final data = await client.recovery(); + await globalState.appController.recoveryData(data, recoveryOption); + return true; + }, + title: appLocalizations.recovery, + ); + if (res != true) return; + globalState.showMessage( + title: appLocalizations.recovery, + message: TextSpan(text: appLocalizations.recoverySuccess), + ); + } + + _handleRecoveryOnWebDAV(BuildContext context, DAVClient client) async { + final recoveryOption = await globalState.showCommonDialog( + child: const RecoveryOptionsDialog(), + ); + if (recoveryOption == null || !context.mounted) return; + _recoveryOnWebDAV(context, client, recoveryOption); + } + + _backupOnLocal(BuildContext context) async { final commonScaffoldState = context.commonScaffoldState; - final res = await commonScaffoldState?.loadingRun(() async { - return await _client?.backup(); - }); + final res = await commonScaffoldState?.loadingRun( + () async { + final backupData = await globalState.appController.backupData(); + await picker.saveFile( + other.getBackupFileName(), + Uint8List.fromList(backupData), + ); + return true; + }, + title: appLocalizations.backup, + ); if (res != true) return; globalState.showMessage( title: appLocalizations.backup, @@ -40,11 +90,24 @@ class _BackupAndRecoveryState extends State { ); } - _recovery(RecoveryOption recoveryOption) async { + _recoveryOnLocal( + BuildContext context, + RecoveryOption recoveryOption, + ) async { + final file = await picker.pickerFile(); + final data = file?.bytes; + if (data == null || !context.mounted) return; final commonScaffoldState = context.commonScaffoldState; - final res = await commonScaffoldState?.loadingRun(() async { - return await _client?.recovery(recoveryOption: recoveryOption); - }); + final res = await commonScaffoldState?.loadingRun( + () async { + await globalState.appController.recoveryData( + List.from(data), + recoveryOption, + ); + return true; + }, + title: appLocalizations.recovery, + ); if (res != true) return; globalState.showMessage( title: appLocalizations.recovery, @@ -52,12 +115,12 @@ class _BackupAndRecoveryState extends State { ); } - _handleRecovery() async { + _handleRecoveryOnLocal(BuildContext context) async { final recoveryOption = await globalState.showCommonDialog( child: const RecoveryOptionsDialog(), ); - if (recoveryOption == null) return; - _recovery(recoveryOption); + if (recoveryOption == null || !context.mounted) return; + _recoveryOnLocal(context, recoveryOption); } @override @@ -65,12 +128,11 @@ class _BackupAndRecoveryState extends State { return Selector( selector: (_, config) => config.dav, builder: (_, dav, __) { - if (dav == null) { - return ListView( - children: [ - ListHeader( - title: appLocalizations.account, - ), + final client = dav != null ? DAVClient(dav) : null; + return ListView( + children: [ + ListHeader(title: appLocalizations.remote), + if (dav == null) ListItem( leading: const Icon(Icons.account_box), title: Text(appLocalizations.noInfo), @@ -83,95 +145,95 @@ class _BackupAndRecoveryState extends State { appLocalizations.bind, ), ), - ), - ], - ); - } - _client = DAVClient(dav); - final pingFuture = _client!.pingCompleter.future; - return ListView( - children: [ - ListHeader(title: appLocalizations.account), - ListItem( - leading: const Icon(Icons.account_box), - title: TooltipText( - text: Text( - dav.user, - maxLines: 1, - overflow: TextOverflow.ellipsis, + ) + else ...[ + ListItem( + leading: const Icon(Icons.account_box), + title: TooltipText( + text: Text( + dav.user, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(appLocalizations.connectivity), - FutureBuilder( - future: pingFuture, - builder: (_, snapshot) { - return Center( - child: FadeBox( - key: const Key("fade_box_1"), - child: snapshot.connectionState == ConnectionState.waiting - ? const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator( - strokeWidth: 1, + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(appLocalizations.connectivity), + FutureBuilder( + future: client!.pingCompleter.future, + builder: (_, snapshot) { + return Center( + child: FadeBox( + child: snapshot.connectionState == + ConnectionState.waiting + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ) + : Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: snapshot.data == true + ? Colors.green + : Colors.red, + ), + width: 12, + height: 12, ), - ) - : Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: snapshot.data == true - ? Colors.green - : Colors.red, - ), - width: 12, - height: 12, - ), - ), - ); - }, - ), - ], + ), + ); + }, + ), + ], + ), + ), + trailing: FilledButton.tonal( + onPressed: () { + _showAddWebDAV(dav); + }, + child: Text( + appLocalizations.edit, + ), ), ), - trailing: FilledButton.tonal( - onPressed: () { - _showAddWebDAV(dav); + const SizedBox( + height: 4, + ), + ListItem( + onTap: () { + _backupOnWebDAV(context, client); }, - child: Text( - appLocalizations.edit, - ), + title: Text(appLocalizations.backup), + subtitle: Text(appLocalizations.remoteBackupDesc), ), + ListItem( + onTap: () { + _handleRecoveryOnWebDAV(context, client); + }, + title: Text(appLocalizations.recovery), + subtitle: Text(appLocalizations.remoteRecoveryDesc), + ), + ], + ListHeader(title: appLocalizations.local), + ListItem( + onTap: () { + _backupOnLocal(context); + }, + title: Text(appLocalizations.backup), + subtitle: Text(appLocalizations.localBackupDesc), ), - FutureBuilder( - future: pingFuture, - builder: (_, snapshot) { - return FadeBox( - key: const Key("fade_box_2"), - child: snapshot.data == true - ? Column( - children: [ - ListHeader( - title: appLocalizations.backupAndRecovery), - ListItem( - onTap: _backup, - title: Text(appLocalizations.backup), - subtitle: Text(appLocalizations.backupDesc), - ), - ListItem( - onTap: _handleRecovery, - title: Text(appLocalizations.recovery), - subtitle: Text(appLocalizations.recoveryDesc), - ), - ], - ) - : Container(), - ); + ListItem( + onTap: () { + _handleRecoveryOnLocal(context); }, + title: Text(appLocalizations.recovery), + subtitle: Text(appLocalizations.localRecoveryDesc), ), ], ); @@ -180,6 +242,50 @@ class _BackupAndRecoveryState extends State { } } +class RecoveryOptionsDialog extends StatefulWidget { + const RecoveryOptionsDialog({super.key}); + + @override + State createState() => _RecoveryOptionsDialogState(); +} + +class _RecoveryOptionsDialogState extends State { + _handleOnTab(RecoveryOption? value) { + if (value == null) return; + Navigator.of(context).pop(value); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.recovery), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + content: SizedBox( + width: 250, + child: Wrap( + children: [ + ListItem( + onTap: () { + _handleOnTab(RecoveryOption.onlyProfiles); + }, + title: Text(appLocalizations.recoveryProfiles), + ), + ListItem( + onTap: () { + _handleOnTab(RecoveryOption.all); + }, + title: Text(appLocalizations.recoveryAll), + ) + ], + ), + ), + ); + } +} + class WebDAVFormDialog extends StatefulWidget { final DAV? dav; @@ -238,7 +344,7 @@ class _WebDAVFormDialogState extends State { children: [ TextFormField( controller: uriController, - maxLines: 2, + maxLines: 5, minLines: 1, decoration: InputDecoration( prefixIcon: const Icon(Icons.link), @@ -313,47 +419,3 @@ class _WebDAVFormDialogState extends State { ); } } - -class RecoveryOptionsDialog extends StatefulWidget { - const RecoveryOptionsDialog({super.key}); - - @override - State createState() => _RecoveryOptionsDialogState(); -} - -class _RecoveryOptionsDialogState extends State { - _handleOnTab(RecoveryOption? value) { - if (value == null) return; - Navigator.of(context).pop(value); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.recovery), - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - ListItem( - onTap: () { - _handleOnTab(RecoveryOption.onlyProfiles); - }, - title: Text(appLocalizations.recoveryProfiles), - ), - ListItem( - onTap: () { - _handleOnTab(RecoveryOption.all); - }, - title: Text(appLocalizations.recoveryAll), - ) - ], - ), - ), - ); - } -} diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index cddf298d1..19438de97 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -137,11 +137,40 @@ class _ConfigFragmentState extends State { } } + _updateKeepAliveInterval(int keepAliveInterval) async { + final newKeepAliveIntervalString = + await globalState.showCommonDialog( + child: KeepAliveIntervalFormDialog( + keepAliveInterval: keepAliveInterval, + ), + ); + if (newKeepAliveIntervalString != null && + newKeepAliveIntervalString != "$keepAliveInterval" && + mounted) { + try { + final newKeepAliveInterval = int.parse(newKeepAliveIntervalString); + if (newKeepAliveInterval <= 0) { + throw "Invalid keepAliveInterval"; + } + globalState.appController.clashConfig.keepAliveInterval = + newKeepAliveInterval; + globalState.appController.updateClashConfigDebounce(); + } catch (e) { + globalState.showMessage( + title: appLocalizations.testUrl, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + } + List _buildAppSection() { return generateSection( title: appLocalizations.app, items: [ - if (Platform.isAndroid)...[ + if (Platform.isAndroid) ...[ Selector( selector: (_, config) => config.allowBypass, builder: (_, allowBypass, __) { @@ -263,6 +292,19 @@ class _ConfigFragmentState extends State { ); }, ), + Selector( + selector: (_, config) => config.keepAliveInterval, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.timer_outlined), + title: Text(appLocalizations.keepAliveIntervalDesc), + subtitle: Text("$value ${appLocalizations.seconds}"), + onTap: () { + _updateKeepAliveInterval(value); + }, + ); + }, + ), Selector( selector: (_, config) => config.testUrl, builder: (_, value, __) { @@ -589,3 +631,64 @@ class _TestUrlFormDialogState extends State { ); } } + +class KeepAliveIntervalFormDialog extends StatefulWidget { + final int keepAliveInterval; + + const KeepAliveIntervalFormDialog({ + super.key, + required this.keepAliveInterval, + }); + + @override + State createState() => + _KeepAliveIntervalFormDialogState(); +} + +class _KeepAliveIntervalFormDialogState + extends State { + late TextEditingController keepAliveIntervalController; + + @override + void initState() { + super.initState(); + keepAliveIntervalController = + TextEditingController(text: "${widget.keepAliveInterval}"); + } + + _handleUpdate() async { + final keepAliveInterval = keepAliveIntervalController.value.text; + if (keepAliveInterval.isEmpty) return; + Navigator.of(context).pop(keepAliveInterval); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.keepAliveIntervalDesc), + content: SizedBox( + width: 300, + child: Wrap( + runSpacing: 16, + children: [ + TextField( + maxLines: 1, + minLines: 1, + controller: keepAliveIntervalController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixText: appLocalizations.seconds, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _handleUpdate, + child: Text(appLocalizations.submit), + ) + ], + ); + } +} diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 8bcfee727..0a53acecc 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -11,6 +11,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'view_profile.dart'; + class EditProfile extends StatefulWidget { final Profile profile; final BuildContext context; @@ -121,7 +123,7 @@ class _EditProfileState extends State { } _uploadProfileFile() async { - final platformFile = await globalState.safeRun(picker.pickerConfigFile); + final platformFile = await globalState.safeRun(picker.pickerFile); if (platformFile?.bytes == null) return; fileData = platformFile?.bytes; fileInfoNotifier.value = fileInfoNotifier.value?.copyWith( @@ -260,23 +262,19 @@ class _EditProfileState extends State { padding: const EdgeInsets.symmetric( vertical: 16, ), - 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, + child: ListView.separated( + padding: kMaterialListPadding.copyWith( + bottom: 72, + ), + itemBuilder: (_, index) { + return items[index]; + }, + separatorBuilder: (_, __) { + return const SizedBox( + height: 24, ); }, + itemCount: items.length, ), ), ), diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 0c70c8d43..6113219ae 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -27,8 +27,6 @@ class ProfilesFragment extends StatefulWidget { class _ProfilesFragmentState extends State { Function? applyConfigDebounce; - List> profileItemKeys = []; - _handleShowAddExtendPage() { showExtendPage( globalState.navigatorKey.currentState!.context, @@ -51,8 +49,28 @@ class _ProfilesFragmentState extends State { } _updateProfiles() async { - final updateProfiles = profileItemKeys.map( - (key) async => await key.currentState?.updateProfile(false)); + final appController = globalState.appController; + final config = appController.config; + final profiles = appController.config.profiles; + final updateProfiles = profiles.map( + (profile) async { + config.setProfile( + profile.copyWith(isUpdating: true), + ); + try { + await appController.updateProfile(profile); + if (profile.id == appController.config.currentProfile?.id) { + appController.applyProfile(isPrue: true); + } + } catch (_) { + config.setProfile( + profile.copyWith( + isUpdating: false, + ), + ); + } + }, + ); await Future.wait(updateProfiles); } @@ -74,19 +92,6 @@ class _ProfilesFragmentState extends State { ); } - _changeProfile(String? id) async { - final appController = globalState.appController; - final config = appController.config; - if (id == config.currentProfileId) return; - config.currentProfileId = id; - applyConfigDebounce ??= debounce(() async { - await appController.applyProfile(); - appController.appState.delayMap = {}; - appController.saveConfigPreferences(); - }); - applyConfigDebounce!(); - } - @override Widget build(BuildContext context) { return FloatLayout( @@ -119,40 +124,33 @@ class _ProfilesFragmentState extends State { label: appLocalizations.nullProfileDesc, ); } - profileItemKeys = state.profiles - .map( - (profile) => GlobalObjectKey<_ProfileItemState>(profile.id)) - .toList(); final columns = _getColumns(state.viewMode); return Align( alignment: Alignment.topCenter, - 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, - ), - ), - ], - ), - ); - }, + child: SingleChildScrollView( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 88, + ), + child: Grid( + mainAxisSpacing: 16, + crossAxisSpacing: 16, + crossAxisCount: columns, + children: [ + for (int i = 0; i < state.profiles.length; i++) + GridItem( + child: ProfileItem( + key: Key(state.profiles[i].id), + profile: state.profiles[i], + groupValue: state.currentProfileId, + onChanged: + globalState.appController.changeProfile, + ), + ), + ], + ), ), ); }, @@ -162,7 +160,7 @@ class _ProfilesFragmentState extends State { } } -class ProfileItem extends StatefulWidget { +class ProfileItem extends StatelessWidget { final Profile profile; final String? groupValue; final void Function(String? value) onChanged; @@ -174,22 +172,15 @@ class ProfileItem extends StatefulWidget { required this.onChanged, }); - @override - State createState() => _ProfileItemState(); -} - -class _ProfileItemState extends State { - final isUpdating = ValueNotifier(false); - - _handleDeleteProfile() async { + _handleDeleteProfile(BuildContext context) async { globalState.showMessage( title: appLocalizations.tip, message: TextSpan( text: appLocalizations.deleteProfileTip, ), onTab: () async { - await globalState.appController.deleteProfile(widget.profile.id); - if (mounted) { + await globalState.appController.deleteProfile(profile.id); + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -200,41 +191,47 @@ class _ProfileItemState extends State { await globalState.safeRun(updateProfile); } - Future updateProfile([isSingle = true]) async { - isUpdating.value = true; - try { - final appController = globalState.appController; - await appController.updateProfile(widget.profile); - if (widget.profile.id == appController.config.currentProfile?.id) { - globalState.appController.applyProfile(isPrue: true); - } - } catch (e) { - isUpdating.value = false; - if (!isSingle) { - return e.toString(); - } else { + Future updateProfile() async { + final appController = globalState.appController; + final config = appController.config; + if (profile.type == ProfileType.file) return; + await globalState.safeRun(() async { + try { + config.setProfile( + profile.copyWith( + isUpdating: true, + ), + ); + await appController.updateProfile(profile); + if (profile.id == appController.config.currentProfile?.id) { + appController.applyProfile(isPrue: true); + } + } catch (e) { + config.setProfile( + profile.copyWith( + isUpdating: false, + ), + ); rethrow; } - } - isUpdating.value = false; - return null; + }); } - _handleShowEditExtendPage() { + _handleShowEditExtendPage(BuildContext context) { showExtendPage( context, body: EditProfile( - profile: widget.profile, + profile: profile, context: context, ), title: "${appLocalizations.edit}${appLocalizations.profile}", ); } - List _buildUserInfo(UserInfo userInfo) { + List _buildUserInfo(BuildContext context, UserInfo userInfo) { final use = userInfo.upload + userInfo.download; final total = userInfo.total; - if(total == 0){ + if (total == 0) { return []; } final useShow = TrafficValue(value: use).show; @@ -261,13 +258,13 @@ class _ProfileItemState extends State { ]; } - List _buildUrlProfileInfo(Profile profile) { + List _buildUrlProfileInfo(BuildContext context) { final userInfo = profile.userInfo; return [ const SizedBox( height: 8, ), - if (userInfo != null) ..._buildUserInfo(userInfo), + if (userInfo != null) ..._buildUserInfo(context, userInfo), Text( profile.lastUpdateDate?.lastUpdateTimeDesc ?? "", style: context.textTheme.labelMedium?.toLight, @@ -275,7 +272,7 @@ class _ProfileItemState extends State { ]; } - List _buildFileProfileInfo(Profile profile) { + List _buildFileProfileInfo(BuildContext context) { return [ const SizedBox( height: 8, @@ -287,50 +284,8 @@ class _ProfileItemState extends State { ]; } - _buildTitle(Profile profile) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - profile.label ?? profile.id, - style: context.textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...switch (profile.type) { - ProfileType.file => _buildFileProfileInfo( - profile, - ), - ProfileType.url => _buildUrlProfileInfo( - profile, - ), - }, - ], - ), - ], - ), - ); - } - - @override - void dispose() { - isUpdating.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - final profile = widget.profile; - final groupValue = widget.groupValue; - final onChanged = widget.onChanged; return CommonCard( isSelected: profile.id == groupValue, onPressed: () { @@ -343,55 +298,75 @@ class _ProfileItemState extends State { trailing: SizedBox( height: 40, width: 40, - child: ValueListenableBuilder( - valueListenable: isUpdating, - builder: (_, isUpdating, ___) { - return FadeBox( - 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.update, - label: appLocalizations.update, - iconData: Icons.sync, - ), - 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; - } - }, + child: FadeBox( + child: profile.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.update, + label: appLocalizations.update, + iconData: Icons.sync, + ), + CommonPopupMenuItem( + action: ProfileActions.delete, + label: appLocalizations.delete, + iconData: Icons.delete, + ), + ], + onSelected: (ProfileActions? action) async { + switch (action) { + case ProfileActions.edit: + _handleShowEditExtendPage(context); + break; + case ProfileActions.delete: + _handleDeleteProfile(context); + break; + case ProfileActions.update: + _handleUpdateProfile(); + break; + case null: + break; + } + }, + ), + ), + ), + title: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + profile.label ?? profile.id, + style: context.textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...switch (profile.type) { + ProfileType.file => _buildFileProfileInfo(context), + ProfileType.url => _buildUrlProfileInfo(context), + }, + ], + ), + ], ), ), - title: _buildTitle(profile), tileTitleAlignment: ListTileTitleAlignment.titleHeight, ), ); diff --git a/lib/fragments/profiles/view_profile.dart b/lib/fragments/profiles/view_profile.dart new file mode 100644 index 000000000..e1b42d8ae --- /dev/null +++ b/lib/fragments/profiles/view_profile.dart @@ -0,0 +1,232 @@ +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/atom-one-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; + final CodeLineEditingController _controller = CodeLineEditingController(); + final key = GlobalKey(); + final _focusNode = FocusNode(); + String? rawText; + + @override + void initState() { + super.initState(); + appPath.getProfilePath(widget.profile.id).then((path) async { + if (path == null) return; + final file = File(path); + rawText = await file.readAsString(); + _controller.text = rawText ?? ""; + }); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + _focusNode.dispose(); + } + + Profile get profile => widget.profile; + + _handleChangeReadOnly() async { + if (readOnly == true) { + setState(() { + readOnly = false; + }); + } else { + if (_controller.text == rawText) return; + final newProfile = await key.currentState?.loadingRun(() async { + return await profile.saveFileWithString(_controller.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), + ), + IconButton( + onPressed: _handleChangeReadOnly, + icon: readOnly ? const Icon(Icons.edit) : const Icon(Icons.save), + ), + ], + body: CodeEditor( + readOnly: readOnly, + focusNode: _focusNode, + scrollbarBuilder: (context, child, details) { + return Scrollbar( + controller: details.controller, + thickness: 8, + radius: const Radius.circular(2), + interactive: true, + child: child, + ); + }, + showCursorWhenReadOnly: false, + controller: _controller, + shortcutsActivatorsBuilder: + const DefaultCodeShortcutsActivatorsBuilder(), + indicatorBuilder: ( + context, + editingController, + chunkController, + notifier, + ) { + return Row( + children: [ + DefaultCodeLineNumber( + controller: editingController, + notifier: notifier, + ), + DefaultCodeChunkIndicator( + width: 20, + controller: chunkController, + notifier: notifier, + ) + ], + ); + }, + toolbarController: + !readOnly ? ContextMenuControllerImpl(_focusNode) : null, + style: CodeEditorStyle( + fontSize: 14, + codeTheme: CodeHighlightTheme( + languages: { + 'yaml': CodeHighlightThemeMode( + mode: langYaml, + ) + }, + theme: atomOneLightTheme, + ), + ), + ), + 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 { + OverlayEntry? _overlayEntry; + + final FocusNode focusNode; + + ContextMenuControllerImpl( + this.focusNode, + ); + + _removeOverLayEntry() { + _overlayEntry?.remove(); + _overlayEntry = null; + } + + @override + void hide(BuildContext context) { + // _removeOverLayEntry(); + } + + _handleCut(CodeLineEditingController controller) { + controller.cut(); + _removeOverLayEntry(); + } + + _handleCopy(CodeLineEditingController controller) async { + await controller.copy(); + _removeOverLayEntry(); + } + + _handlePaste(CodeLineEditingController controller) { + controller.paste(); + _removeOverLayEntry(); + } + + @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; + } + _removeOverLayEntry(); + final relativeRect = RelativeRect.fromSize( + (anchors.primaryAnchor) & + const Size(150, double.infinity), + MediaQuery.of(context).size, + ); + _overlayEntry ??= OverlayEntry( + builder: (context) => ValueListenableBuilder( + valueListenable: controller, + builder: (_, __, child) { + if (controller.selectedText.isEmpty) { + _removeOverLayEntry(); + } + return child!; + }, + child: Positioned( + left: relativeRect.left, + top: relativeRect.top, + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(focusNode); + }, + child: Container( + width: 200, + height: 200, + color: Colors.green, + ), + ), + ), + ), + ), + ); + Overlay.of(context).insert(_overlayEntry!); + } +} diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart index 629d0dd1a..46dfca682 100644 --- a/lib/fragments/proxies/common.dart +++ b/lib/fragments/proxies/common.dart @@ -42,7 +42,7 @@ double getItemHeight(ProxyCardType proxyCardType) { delayTest(List proxies) async { final appController = globalState.appController; - for (final proxy in proxies) { + final delayProxies = proxies.map((proxy) async { final proxyName = appController.appState.getRealProxyName(proxy.name); globalState.appController.setDelay( Delay( @@ -50,11 +50,9 @@ delayTest(List proxies) async { value: 0, ), ); - clashCore.getDelay(proxyName).then((delay) { - globalState.appController.setDelay(delay); - }); - } - await Future.delayed(httpTimeoutDuration + moreDuration); + globalState.appController.setDelay(await clashCore.getDelay(proxyName)); + }); + await Future.wait(delayProxies); appController.appState.sortNum++; } diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart new file mode 100644 index 000000000..dd439cf2b --- /dev/null +++ b/lib/fragments/proxies/providers.dart @@ -0,0 +1,187 @@ +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/app.dart'; +import 'package:fl_clash/models/ffi.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +typedef UpdatingMap = Map; + +class Providers extends StatefulWidget { + const Providers({ + super.key, + }); + + @override + State createState() => _ProvidersState(); +} + +class _ProvidersState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.actions = [ + IconButton( + onPressed: () { + _updateProviders(); + }, + icon: const Icon( + Icons.sync, + ), + ) + ]; + }, + ); + } + + _updateProviders() async { + final appState = globalState.appController.appState; + final providers = globalState.appController.appState.providers; + final updateProviders = providers.map( + (provider) async { + appState.setProvider( + provider.copyWith(isUpdating: true), + ); + await clashCore.updateExternalProvider( + providerName: provider.name, + providerType: provider.type, + ); + appState.setProvider( + clashCore.getExternalProvider(provider.name), + ); + }, + ); + await Future.wait(updateProviders); + } + + @override + Widget build(BuildContext context) { + return Selector>( + selector: (_, appState) => appState.providers, + builder: (_, providers, ___) { + return ListView.separated( + itemBuilder: (_, index) { + return ProviderItem( + provider: providers[index], + ); + }, + separatorBuilder: (_, index) { + return const Divider( + height: 0, + ); + }, + itemCount: providers.length, + ); + }, + ); + } +} + +class ProviderItem extends StatelessWidget { + final ExternalProvider provider; + + const ProviderItem({ + super.key, + required this.provider, + }); + + _handleUpdateProfile() async { + await globalState.safeRun(updateProvider); + } + + updateProvider() async { + final appState = globalState.appController.appState; + if (provider.vehicleType != "HTTP") return; + await globalState.safeRun(() async { + appState.setProvider( + provider.copyWith( + isUpdating: true, + ), + ); + final message = await clashCore.updateExternalProvider( + providerName: provider.name, + providerType: provider.type, + ); + if (message.isNotEmpty) throw message; + }); + appState.setProvider( + clashCore.getExternalProvider(provider.name), + ); + } + + String _buildProviderDesc() { + final baseInfo = + "${provider.type}(${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}"; + final count = provider.count; + return switch (count == 0) { + true => baseInfo, + false => "$baseInfo · $count${appLocalizations.entries}", + }; + } + + @override + Widget build(BuildContext context) { + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + title: Text(provider.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + Text( + _buildProviderDesc(), + ), + Text( + provider.path, + style: context.textTheme.bodyMedium?.toLight, + ), + const SizedBox( + height: 8, + ), + Wrap( + runSpacing: 6, + spacing: 12, + children: [ + // CommonChip( + // avatar: const Icon(Icons.upload), + // label: appLocalizations.upload, + // onPressed: () {}, + // ), + if (provider.vehicleType == "HTTP") + CommonChip( + avatar: const Icon(Icons.sync), + label: appLocalizations.sync, + onPressed: () { + _handleUpdateProfile(); + }, + ), + ], + ), + ], + ), + trailing: SizedBox( + height: 48, + width: 48, + child: FadeBox( + child: provider.isUpdating + ? const Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : const SizedBox(), + ), + ), + ); + } +} diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index 2ce2b28cd..31ee95325 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -6,6 +6,7 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'providers.dart'; import 'setting.dart'; import 'tab.dart'; @@ -19,18 +20,37 @@ class ProxiesFragment extends StatefulWidget { class _ProxiesFragmentState extends State { final GlobalKey _proxiesTabKey = GlobalKey(); - _initActions(ProxiesType proxiesType) { + _initActions(ProxiesType proxiesType, bool hasProvider) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final commonScaffoldState = context.findAncestorStateOfType(); commonScaffoldState?.actions = [ + if (hasProvider) ...[ + IconButton( + onPressed: () { + showExtendPage( + forceNotSide: true, + extendPageWidth: 360, + context, + body: const Providers(), + title: appLocalizations.externalResources, + ); + }, + icon: const Icon( + Icons.swap_vert_circle_outlined, + ), + ), + const SizedBox( + width: 8, + ), + ], if (proxiesType == ProxiesType.tab) ...[ IconButton( onPressed: () { _proxiesTabKey.currentState?.scrollToGroupSelected(); }, icon: const Icon( - Icons.gps_fixed, + Icons.adjust_outlined, ), ), const SizedBox( @@ -60,18 +80,18 @@ class _ProxiesFragmentState extends State { return Selector( selector: (_, config) => config.proxiesType, builder: (_, proxiesType, __) { - return Selector( - selector: (_, appState) => appState.currentLabel == 'proxies', - builder: (_, isCurrent, child) { - if (isCurrent) { - _initActions(proxiesType); + return ProxiesActionsBuilder( + builder: (state, child) { + if (state.isCurrent) { + _initActions(proxiesType, state.hasProvider); } - return switch (proxiesType) { - ProxiesType.tab => ProxiesTabFragment( - key: _proxiesTabKey, - ), - ProxiesType.list => const ProxiesListFragment(), - }; + return child!; + }, + child: switch (proxiesType) { + ProxiesType.tab => ProxiesTabFragment( + key: _proxiesTabKey, + ), + ProxiesType.list => const ProxiesListFragment(), }, ); }, diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index 0bbda9d09..d6a651f03 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -22,80 +22,11 @@ class GeoItem { }); } -class Resources extends StatefulWidget { +class Resources extends StatelessWidget { const Resources({super.key}); @override - State createState() => _ResourcesState(); -} - -class _ResourcesState extends State { - List externalProviders = []; - - List> providerItemKeys = []; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _syncExternalProviders(); - }); - } - - _syncExternalProviders() async { - externalProviders = await clashCore.getExternalProviders(); - if (mounted) { - setState(() {}); - } - } - - _updateProviders() async { - final updateProviders = providerItemKeys.map( - (key) async => await key.currentState?.updateProvider(false), - ); - await Future.wait(updateProviders); - _syncExternalProviders(); - } - - List _buildExternalProviderSection() { - List> keys = []; - final res = generateInfoSection( - info: Info( - iconData: Icons.source, - label: appLocalizations.externalResources, - ), - actions: [ - IconButton.filledTonal( - onPressed: () { - _updateProviders(); - }, - padding: const EdgeInsets.all(4), - iconSize: 20, - icon: const Icon( - Icons.sync, - ), - ) - ], - items: externalProviders.map( - (externalProvider) { - final key = - GlobalObjectKey<_ProviderItemState>(externalProvider.name); - keys.add(key); - return ProviderItem( - key: key, - provider: externalProvider, - onUpdated: () { - _syncExternalProviders(); - }, - ); - }, - ), - ); - providerItemKeys = keys; - return res; - } - - List _buildGeoDataSection() { + Widget build(BuildContext context) { const geoItems = [ GeoItem( label: "GeoIp", @@ -111,26 +42,19 @@ class _ResourcesState extends State { GeoItem(label: "ASN", fileName: asnFileName, key: "asn"), ]; - return generateInfoSection( - info: Info( - iconData: Icons.storage, - label: appLocalizations.geoData, - ), - items: geoItems.map( - (geoItem) => GeoDataListItem( + return ListView.separated( + itemBuilder: (_, index) { + final geoItem = geoItems[index]; + return GeoDataListItem( geoItem: geoItem, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return generateListView( - [ - ..._buildGeoDataSection(), - ..._buildExternalProviderSection(), - ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 0, + ); + }, + itemCount: geoItems.length, ); } } @@ -226,9 +150,6 @@ class _GeoDataListItemState extends State { const SizedBox( height: 8, ), - const SizedBox( - height: 8, - ), Wrap( runSpacing: 6, spacing: 12, @@ -315,117 +236,6 @@ class _GeoDataListItemState extends State { } } -class ProviderItem extends StatefulWidget { - final ExternalProvider provider; - final Function onUpdated; - - const ProviderItem({ - super.key, - required this.provider, - required this.onUpdated, - }); - - @override - State createState() => _ProviderItemState(); -} - -class _ProviderItemState extends State { - final isUpdating = ValueNotifier(false); - - ExternalProvider get provider => widget.provider; - - _handleUpdateProfile() async { - await globalState.safeRun(updateProvider); - widget.onUpdated(); - } - - updateProvider([isSingle = true]) async { - if (provider.vehicleType != "HTTP") return; - isUpdating.value = true; - try { - final message = await clashCore.updateExternalProvider( - providerName: provider.name, - providerType: provider.type, - ); - if (message.isNotEmpty) throw message; - } catch (e) { - isUpdating.value = false; - if (!isSingle) { - return e.toString(); - } else { - rethrow; - } - } - isUpdating.value = false; - return null; - } - - String _buildProviderDesc() { - return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}"; - } - - @override - void dispose() { - super.dispose(); - isUpdating.dispose(); - } - - Widget _buildSubtitle() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 4, - ), - Text( - _buildProviderDesc(), - ), - if (provider.vehicleType == "HTTP") ...[ - const SizedBox( - height: 8, - ), - CommonChip( - avatar: const Icon(Icons.sync), - label: appLocalizations.sync, - onPressed: () { - _handleUpdateProfile(); - }, - ), - ], - ], - ); - } - - @override - Widget build(BuildContext context) { - return ListItem( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - title: Text(provider.name), - subtitle: _buildSubtitle(), - trailing: SizedBox( - height: 48, - width: 48, - child: ValueListenableBuilder( - valueListenable: isUpdating, - builder: (_, isUpdating, ___) { - return FadeBox( - child: isUpdating - ? const Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ) - : const SizedBox(), - ); - }, - ), - ), - ); - } -} - class UpdateGeoUrlFormDialog extends StatefulWidget { final String title; final String url; diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 7a59f2012..43156a6d1 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -66,6 +66,7 @@ "hours": "Hours", "days": "Days", "minutes": "Minutes", + "seconds": "Seconds", "ago": " Ago", "just": "Just", "qrcode": "QR code", @@ -130,12 +131,10 @@ "notSelectedTip": "The current proxy group cannot be selected.", "tip": "tip", "backupAndRecovery": "Backup and Recovery", - "backupAndRecoveryDesc": "Sync data by WebDAV", + "backupAndRecoveryDesc": "Sync data via WebDAV or file", "account": "Account", "backup": "Backup", - "backupDesc": "Backup local data to WebDAV", "recovery": "Recovery", - "recoveryDesc": "Recovery data from WebDAV", "recoveryProfiles": "Only recovery profiles", "recoveryAll": "Recovery all data", "recoverySuccess": "Recovery success", @@ -220,5 +219,13 @@ "onlyStatisticsProxy": "Only statistics proxy", "onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic", "deleteProfileTip": "Sure you want to delete the current profile?", - "prueBlackMode": "Prue black mode" + "prueBlackMode": "Prue black mode", + "keepAliveIntervalDesc": "Tcp keep alive interval", + "entries": " entries", + "local": "Local", + "remote": "Remote", + "remoteBackupDesc": "Backup local data to WebDAV", + "remoteRecoveryDesc": "Recovery data from WebDAV", + "localBackupDesc": "Backup local data to local", + "localRecoveryDesc": "Recovery data from file" } \ 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 f8e600921..7a925b623 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -66,6 +66,7 @@ "hours": "小时", "days": "天", "minutes": "分钟", + "seconds": "秒", "ago": "前", "just": "刚刚", "qrcode": "二维码", @@ -130,12 +131,10 @@ "notSelectedTip": "当前代理组无法选中", "tip": "提示", "backupAndRecovery": "备份与恢复", - "backupAndRecoveryDesc": "通过WebDAV同步数据", + "backupAndRecoveryDesc": "通过WebDAV或者文件同步数据", "account": "账号", "backup": "备份", - "backupDesc": "备份数据到WebDAV", "recovery": "恢复", - "recoveryDesc": "从WebDAV恢复数据", "recoveryProfiles": "仅恢复配置文件", "recoveryAll": "恢复所有数据", "recoverySuccess": "恢复成功", @@ -220,5 +219,13 @@ "onlyStatisticsProxy": "仅统计代理", "onlyStatisticsProxyDesc": "开启后,将只统计代理流量", "deleteProfileTip": "确定要删除当前配置吗?", - "prueBlackMode": "纯黑模式" + "prueBlackMode": "纯黑模式", + "keepAliveIntervalDesc": "TCP保持活动间隔", + "entries": "个条目", + "local": "本地", + "remote": "远程", + "remoteBackupDesc": "备份数据到WebDAV", + "remoteRecoveryDesc": "通过WebDAV恢复数据", + "localBackupDesc": "备份数据到本地", + "localRecoveryDesc": "通过文件恢复数据" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index a5f56b791..885c3951b 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -74,10 +74,8 @@ class MessageLookup extends MessageLookupByLibrary { "backup": MessageLookupByLibrary.simpleMessage("Backup"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("Backup and Recovery"), - "backupAndRecoveryDesc": - MessageLookupByLibrary.simpleMessage("Sync data by WebDAV"), - "backupDesc": - MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"), + "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage( + "Sync data via WebDAV or file"), "backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"), "bind": MessageLookupByLibrary.simpleMessage("Bind"), "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"), @@ -129,6 +127,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("Download"), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "en": MessageLookupByLibrary.simpleMessage("English"), + "entries": MessageLookupByLibrary.simpleMessage(" entries"), "exclude": MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"), "excludeDesc": MessageLookupByLibrary.simpleMessage( @@ -172,9 +171,16 @@ class MessageLookup extends MessageLookupByLibrary { "ipv6Desc": MessageLookupByLibrary.simpleMessage( "When turned on it will be able to receive IPv6 traffic"), "just": MessageLookupByLibrary.simpleMessage("Just"), + "keepAliveIntervalDesc": + MessageLookupByLibrary.simpleMessage("Tcp keep alive interval"), "language": MessageLookupByLibrary.simpleMessage("Language"), "light": MessageLookupByLibrary.simpleMessage("Light"), "list": MessageLookupByLibrary.simpleMessage("List"), + "local": MessageLookupByLibrary.simpleMessage("Local"), + "localBackupDesc": + MessageLookupByLibrary.simpleMessage("Backup local data to local"), + "localRecoveryDesc": + MessageLookupByLibrary.simpleMessage("Recovery data from file"), "logLevel": MessageLookupByLibrary.simpleMessage("LogLevel"), "logcat": MessageLookupByLibrary.simpleMessage("Logcat"), "logcatDesc": MessageLookupByLibrary.simpleMessage( @@ -265,12 +271,15 @@ class MessageLookup extends MessageLookupByLibrary { "recovery": MessageLookupByLibrary.simpleMessage("Recovery"), "recoveryAll": MessageLookupByLibrary.simpleMessage("Recovery all data"), - "recoveryDesc": - MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("Only recovery profiles"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("Recovery success"), + "remote": MessageLookupByLibrary.simpleMessage("Remote"), + "remoteBackupDesc": + MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"), + "remoteRecoveryDesc": + MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"), "requests": MessageLookupByLibrary.simpleMessage("Requests"), "requestsDesc": MessageLookupByLibrary.simpleMessage( "View recently request records"), @@ -280,6 +289,7 @@ class MessageLookup extends MessageLookupByLibrary { "rule": MessageLookupByLibrary.simpleMessage("Rule"), "save": MessageLookupByLibrary.simpleMessage("Save"), "search": MessageLookupByLibrary.simpleMessage("Search"), + "seconds": MessageLookupByLibrary.simpleMessage("Seconds"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selected": MessageLookupByLibrary.simpleMessage("Selected"), "settings": MessageLookupByLibrary.simpleMessage("Settings"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 6cb107a18..99e2e58fe 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -62,8 +62,7 @@ class MessageLookup extends MessageLookupByLibrary { "backup": MessageLookupByLibrary.simpleMessage("备份"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRecoveryDesc": - MessageLookupByLibrary.simpleMessage("通过WebDAV同步数据"), - "backupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), + MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), "bind": MessageLookupByLibrary.simpleMessage("绑定"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), @@ -106,6 +105,7 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("下载"), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "en": MessageLookupByLibrary.simpleMessage("英语"), + "entries": MessageLookupByLibrary.simpleMessage("个条目"), "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), @@ -139,9 +139,14 @@ class MessageLookup extends MessageLookupByLibrary { "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), + "keepAliveIntervalDesc": + MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "language": MessageLookupByLibrary.simpleMessage("语言"), "light": MessageLookupByLibrary.simpleMessage("浅色"), "list": MessageLookupByLibrary.simpleMessage("列表"), + "local": MessageLookupByLibrary.simpleMessage("本地"), + "localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"), + "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), @@ -213,9 +218,12 @@ class MessageLookup extends MessageLookupByLibrary { "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), - "recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), + "remote": MessageLookupByLibrary.simpleMessage("远程"), + "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), + "remoteRecoveryDesc": + MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"), "requests": MessageLookupByLibrary.simpleMessage("请求"), "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "resources": MessageLookupByLibrary.simpleMessage("资源"), @@ -223,6 +231,7 @@ class MessageLookup extends MessageLookupByLibrary { "rule": MessageLookupByLibrary.simpleMessage("规则"), "save": MessageLookupByLibrary.simpleMessage("保存"), "search": MessageLookupByLibrary.simpleMessage("搜索"), + "seconds": MessageLookupByLibrary.simpleMessage("秒"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selected": MessageLookupByLibrary.simpleMessage("已选择"), "settings": MessageLookupByLibrary.simpleMessage("设置"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index f4c99dccd..796b6fac3 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -720,6 +720,16 @@ class AppLocalizations { ); } + /// `Seconds` + String get seconds { + return Intl.message( + 'Seconds', + name: 'seconds', + desc: '', + args: [], + ); + } + /// ` Ago` String get ago { return Intl.message( @@ -1360,10 +1370,10 @@ class AppLocalizations { ); } - /// `Sync data by WebDAV` + /// `Sync data via WebDAV or file` String get backupAndRecoveryDesc { return Intl.message( - 'Sync data by WebDAV', + 'Sync data via WebDAV or file', name: 'backupAndRecoveryDesc', desc: '', args: [], @@ -1390,16 +1400,6 @@ class AppLocalizations { ); } - /// `Backup local data to WebDAV` - String get backupDesc { - return Intl.message( - 'Backup local data to WebDAV', - name: 'backupDesc', - desc: '', - args: [], - ); - } - /// `Recovery` String get recovery { return Intl.message( @@ -1410,16 +1410,6 @@ class AppLocalizations { ); } - /// `Recovery data from WebDAV` - String get recoveryDesc { - return Intl.message( - 'Recovery data from WebDAV', - name: 'recoveryDesc', - desc: '', - args: [], - ); - } - /// `Only recovery profiles` String get recoveryProfiles { return Intl.message( @@ -2269,6 +2259,86 @@ class AppLocalizations { args: [], ); } + + /// `Tcp keep alive interval` + String get keepAliveIntervalDesc { + return Intl.message( + 'Tcp keep alive interval', + name: 'keepAliveIntervalDesc', + desc: '', + args: [], + ); + } + + /// ` entries` + String get entries { + return Intl.message( + ' entries', + name: 'entries', + desc: '', + args: [], + ); + } + + /// `Local` + String get local { + return Intl.message( + 'Local', + name: 'local', + desc: '', + args: [], + ); + } + + /// `Remote` + String get remote { + return Intl.message( + 'Remote', + name: 'remote', + desc: '', + args: [], + ); + } + + /// `Backup local data to WebDAV` + String get remoteBackupDesc { + return Intl.message( + 'Backup local data to WebDAV', + name: 'remoteBackupDesc', + desc: '', + args: [], + ); + } + + /// `Recovery data from WebDAV` + String get remoteRecoveryDesc { + return Intl.message( + 'Recovery data from WebDAV', + name: 'remoteRecoveryDesc', + desc: '', + args: [], + ); + } + + /// `Backup local data to local` + String get localBackupDesc { + return Intl.message( + 'Backup local data to local', + name: 'localBackupDesc', + desc: '', + args: [], + ); + } + + /// `Recovery data from file` + String get localRecoveryDesc { + return Intl.message( + 'Recovery data from file', + name: 'localRecoveryDesc', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/models/app.dart b/lib/models/app.dart index 081f64929..071ebd3e7 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -8,6 +8,7 @@ import 'connection.dart'; import 'ffi.dart'; import 'log.dart'; import 'navigation.dart'; +import 'package.dart'; import 'profile.dart'; import 'proxy.dart'; import 'system_color_scheme.dart'; @@ -35,6 +36,8 @@ class AppState with ChangeNotifier { double _viewWidth; List _requests; num _checkIpNum; + List _providers; + List _packages; AppState({ required Mode mode, @@ -54,6 +57,8 @@ class AppState with ChangeNotifier { _totalTraffic = Traffic(), _delayMap = {}, _groups = [], + _providers = [], + _packages = [], _isCompatible = isCompatible, _systemColorSchemes = const SystemColorSchemes(); @@ -330,6 +335,31 @@ class AppState with ChangeNotifier { } } + List get packages => _packages; + + set packages(List value) { + if (!const ListEquality().equals(_packages, value)) { + _packages = value; + notifyListeners(); + } + } + + List get providers => _providers; + + set providers(List value) { + if (!const ListEquality().equals(_providers, value)) { + _providers = value; + notifyListeners(); + } + } + + setProvider(ExternalProvider provider) { + final index = _providers.indexWhere((item) => item.name == provider.name); + if (index == -1) return; + _providers = List.from(_providers)..[index] = provider; + notifyListeners(); + } + Group? getGroupWithName(String groupName) { final index = currentGroups.indexWhere((element) => element.name == groupName); diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 4d1b3bc24..6aa4cfcc3 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -119,6 +119,7 @@ class ClashConfig extends ChangeNotifier { String _externalController; Mode _mode; FindProcessMode _findProcessMode; + int _keepAliveInterval; bool _unifiedDelay; bool _tcpConcurrent; Tun _tun; @@ -139,6 +140,7 @@ class ClashConfig extends ChangeNotifier { _unifiedDelay = false, _geodataLoader = geodataLoaderMemconservative, _externalController = '', + _keepAliveInterval = 30, _dns = Dns(), _geoXUrl = defaultGeoXMap, _rules = []; @@ -203,6 +205,16 @@ class ClashConfig extends ChangeNotifier { } } + @JsonKey(name: "keep-alive-interval", defaultValue: 30) + int get keepAliveInterval => _keepAliveInterval; + + set keepAliveInterval(int value) { + if (_keepAliveInterval != value) { + _keepAliveInterval = value; + notifyListeners(); + } + } + @JsonKey(defaultValue: false) bool get ipv6 => _ipv6; diff --git a/lib/models/config.dart b/lib/models/config.dart index 5384b67a1..11147fe5e 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -29,6 +29,7 @@ class AccessControl with _$AccessControl { class CoreState with _$CoreState { const factory CoreState({ AccessControl? accessControl, + required String currentProfileName, required bool allowBypass, required bool systemProxy, required int mixedPort, diff --git a/lib/models/ffi.dart b/lib/models/ffi.dart index bd0bcb91c..32510f0e9 100644 --- a/lib/models/ffi.dart +++ b/lib/models/ffi.dart @@ -24,7 +24,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams { @freezed class UpdateConfigParams with _$UpdateConfigParams { const factory UpdateConfigParams({ - @JsonKey(name: "profile-path") String? profilePath, + @JsonKey(name: "profile-id") required String profileId, required ClashConfig config, required ConfigExtendedParams params, }) = _UpdateConfigParams; @@ -123,6 +123,9 @@ class ExternalProvider with _$ExternalProvider { const factory ExternalProvider({ required String name, required String type, + required String path, + required int count, + @Default(false) bool isUpdating, @JsonKey(name: "vehicle-type") required String vehicleType, @JsonKey(name: "update-at") required DateTime updateAt, }) = _ExternalProvider; @@ -140,7 +143,7 @@ abstract mixin class AppMessageListener { void onStarted(String runTime) {} - void onLoaded(String groupName) {} + void onLoaded(String providerName) {} } abstract mixin class ServiceMessageListener { @@ -150,7 +153,5 @@ abstract mixin class ServiceMessageListener { onStarted(String runTime) {} - onLoaded(String groupName) {} + onLoaded(String providerName) {} } - - diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 006a934ec..5e4c183da 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -45,6 +45,7 @@ ClashConfig _$ClashConfigFromJson(Map json) => ClashConfig() ..logLevel = $enumDecodeNullable(_$LogLevelEnumMap, json['log-level']) ?? LogLevel.info ..externalController = json['external-controller'] as String? ?? '' + ..keepAliveInterval = (json['keep-alive-interval'] as num?)?.toInt() ?? 30 ..ipv6 = json['ipv6'] as bool? ?? false ..geodataLoader = json['geodata-loader'] as String? ?? 'memconservative' ..unifiedDelay = json['unified-delay'] as bool? ?? false @@ -75,6 +76,7 @@ Map _$ClashConfigToJson(ClashConfig instance) => 'allow-lan': instance.allowLan, 'log-level': _$LogLevelEnumMap[instance.logLevel]!, 'external-controller': instance.externalController, + 'keep-alive-interval': instance.keepAliveInterval, 'ipv6': instance.ipv6, 'geodata-loader': instance.geodataLoader, 'unified-delay': instance.unifiedDelay, diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index b708a27a6..18e0567ac 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -247,6 +247,7 @@ CoreState _$CoreStateFromJson(Map json) { /// @nodoc mixin _$CoreState { AccessControl? get accessControl => throw _privateConstructorUsedError; + String get currentProfileName => throw _privateConstructorUsedError; bool get allowBypass => throw _privateConstructorUsedError; bool get systemProxy => throw _privateConstructorUsedError; int get mixedPort => throw _privateConstructorUsedError; @@ -265,6 +266,7 @@ abstract class $CoreStateCopyWith<$Res> { @useResult $Res call( {AccessControl? accessControl, + String currentProfileName, bool allowBypass, bool systemProxy, int mixedPort, @@ -287,6 +289,7 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> @override $Res call({ Object? accessControl = freezed, + Object? currentProfileName = null, Object? allowBypass = null, Object? systemProxy = null, Object? mixedPort = null, @@ -297,6 +300,10 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState> ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable as AccessControl?, + currentProfileName: null == currentProfileName + ? _value.currentProfileName + : currentProfileName // ignore: cast_nullable_to_non_nullable + as String, allowBypass: null == allowBypass ? _value.allowBypass : allowBypass // ignore: cast_nullable_to_non_nullable @@ -339,6 +346,7 @@ abstract class _$$CoreStateImplCopyWith<$Res> @useResult $Res call( {AccessControl? accessControl, + String currentProfileName, bool allowBypass, bool systemProxy, int mixedPort, @@ -360,6 +368,7 @@ class __$$CoreStateImplCopyWithImpl<$Res> @override $Res call({ Object? accessControl = freezed, + Object? currentProfileName = null, Object? allowBypass = null, Object? systemProxy = null, Object? mixedPort = null, @@ -370,6 +379,10 @@ class __$$CoreStateImplCopyWithImpl<$Res> ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable as AccessControl?, + currentProfileName: null == currentProfileName + ? _value.currentProfileName + : currentProfileName // ignore: cast_nullable_to_non_nullable + as String, allowBypass: null == allowBypass ? _value.allowBypass : allowBypass // ignore: cast_nullable_to_non_nullable @@ -395,6 +408,7 @@ class __$$CoreStateImplCopyWithImpl<$Res> class _$CoreStateImpl implements _CoreState { const _$CoreStateImpl( {this.accessControl, + required this.currentProfileName, required this.allowBypass, required this.systemProxy, required this.mixedPort, @@ -406,6 +420,8 @@ class _$CoreStateImpl implements _CoreState { @override final AccessControl? accessControl; @override + final String currentProfileName; + @override final bool allowBypass; @override final bool systemProxy; @@ -416,7 +432,7 @@ class _$CoreStateImpl implements _CoreState { @override String toString() { - return 'CoreState(accessControl: $accessControl, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)'; + return 'CoreState(accessControl: $accessControl, currentProfileName: $currentProfileName, allowBypass: $allowBypass, systemProxy: $systemProxy, mixedPort: $mixedPort, onlyProxy: $onlyProxy)'; } @override @@ -426,6 +442,8 @@ class _$CoreStateImpl implements _CoreState { other is _$CoreStateImpl && (identical(other.accessControl, accessControl) || other.accessControl == accessControl) && + (identical(other.currentProfileName, currentProfileName) || + other.currentProfileName == currentProfileName) && (identical(other.allowBypass, allowBypass) || other.allowBypass == allowBypass) && (identical(other.systemProxy, systemProxy) || @@ -438,8 +456,8 @@ class _$CoreStateImpl implements _CoreState { @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, accessControl, allowBypass, - systemProxy, mixedPort, onlyProxy); + int get hashCode => Object.hash(runtimeType, accessControl, + currentProfileName, allowBypass, systemProxy, mixedPort, onlyProxy); @JsonKey(ignore: true) @override @@ -458,6 +476,7 @@ class _$CoreStateImpl implements _CoreState { abstract class _CoreState implements CoreState { const factory _CoreState( {final AccessControl? accessControl, + required final String currentProfileName, required final bool allowBypass, required final bool systemProxy, required final int mixedPort, @@ -469,6 +488,8 @@ abstract class _CoreState implements CoreState { @override AccessControl? get accessControl; @override + String get currentProfileName; + @override bool get allowBypass; @override bool get systemProxy; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index db7095823..dff1c142a 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -139,6 +139,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map json) => ? null : AccessControl.fromJson( json['accessControl'] as Map), + currentProfileName: json['currentProfileName'] as String, allowBypass: json['allowBypass'] as bool, systemProxy: json['systemProxy'] as bool, mixedPort: (json['mixedPort'] as num).toInt(), @@ -148,6 +149,7 @@ _$CoreStateImpl _$$CoreStateImplFromJson(Map json) => Map _$$CoreStateImplToJson(_$CoreStateImpl instance) => { 'accessControl': instance.accessControl, + 'currentProfileName': instance.currentProfileName, 'allowBypass': instance.allowBypass, 'systemProxy': instance.systemProxy, 'mixedPort': instance.mixedPort, diff --git a/lib/models/generated/ffi.freezed.dart b/lib/models/generated/ffi.freezed.dart index 346eb6015..8174de3bd 100644 --- a/lib/models/generated/ffi.freezed.dart +++ b/lib/models/generated/ffi.freezed.dart @@ -248,8 +248,8 @@ UpdateConfigParams _$UpdateConfigParamsFromJson(Map json) { /// @nodoc mixin _$UpdateConfigParams { - @JsonKey(name: "profile-path") - String? get profilePath => throw _privateConstructorUsedError; + @JsonKey(name: "profile-id") + String get profileId => throw _privateConstructorUsedError; ClashConfig get config => throw _privateConstructorUsedError; ConfigExtendedParams get params => throw _privateConstructorUsedError; @@ -266,7 +266,7 @@ abstract class $UpdateConfigParamsCopyWith<$Res> { _$UpdateConfigParamsCopyWithImpl<$Res, UpdateConfigParams>; @useResult $Res call( - {@JsonKey(name: "profile-path") String? profilePath, + {@JsonKey(name: "profile-id") String profileId, ClashConfig config, ConfigExtendedParams params}); @@ -286,15 +286,15 @@ class _$UpdateConfigParamsCopyWithImpl<$Res, $Val extends UpdateConfigParams> @pragma('vm:prefer-inline') @override $Res call({ - Object? profilePath = freezed, + Object? profileId = null, Object? config = null, Object? params = null, }) { return _then(_value.copyWith( - profilePath: freezed == profilePath - ? _value.profilePath - : profilePath // ignore: cast_nullable_to_non_nullable - as String?, + profileId: null == profileId + ? _value.profileId + : profileId // ignore: cast_nullable_to_non_nullable + as String, config: null == config ? _value.config : config // ignore: cast_nullable_to_non_nullable @@ -324,7 +324,7 @@ abstract class _$$UpdateConfigParamsImplCopyWith<$Res> @override @useResult $Res call( - {@JsonKey(name: "profile-path") String? profilePath, + {@JsonKey(name: "profile-id") String profileId, ClashConfig config, ConfigExtendedParams params}); @@ -343,15 +343,15 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? profilePath = freezed, + Object? profileId = null, Object? config = null, Object? params = null, }) { return _then(_$UpdateConfigParamsImpl( - profilePath: freezed == profilePath - ? _value.profilePath - : profilePath // ignore: cast_nullable_to_non_nullable - as String?, + profileId: null == profileId + ? _value.profileId + : profileId // ignore: cast_nullable_to_non_nullable + as String, config: null == config ? _value.config : config // ignore: cast_nullable_to_non_nullable @@ -368,7 +368,7 @@ class __$$UpdateConfigParamsImplCopyWithImpl<$Res> @JsonSerializable() class _$UpdateConfigParamsImpl implements _UpdateConfigParams { const _$UpdateConfigParamsImpl( - {@JsonKey(name: "profile-path") this.profilePath, + {@JsonKey(name: "profile-id") required this.profileId, required this.config, required this.params}); @@ -376,8 +376,8 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { _$$UpdateConfigParamsImplFromJson(json); @override - @JsonKey(name: "profile-path") - final String? profilePath; + @JsonKey(name: "profile-id") + final String profileId; @override final ClashConfig config; @override @@ -385,7 +385,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { @override String toString() { - return 'UpdateConfigParams(profilePath: $profilePath, config: $config, params: $params)'; + return 'UpdateConfigParams(profileId: $profileId, config: $config, params: $params)'; } @override @@ -393,15 +393,15 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { return identical(this, other) || (other.runtimeType == runtimeType && other is _$UpdateConfigParamsImpl && - (identical(other.profilePath, profilePath) || - other.profilePath == profilePath) && + (identical(other.profileId, profileId) || + other.profileId == profileId) && (identical(other.config, config) || other.config == config) && (identical(other.params, params) || other.params == params)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, profilePath, config, params); + int get hashCode => Object.hash(runtimeType, profileId, config, params); @JsonKey(ignore: true) @override @@ -420,7 +420,7 @@ class _$UpdateConfigParamsImpl implements _UpdateConfigParams { abstract class _UpdateConfigParams implements UpdateConfigParams { const factory _UpdateConfigParams( - {@JsonKey(name: "profile-path") final String? profilePath, + {@JsonKey(name: "profile-id") required final String profileId, required final ClashConfig config, required final ConfigExtendedParams params}) = _$UpdateConfigParamsImpl; @@ -428,8 +428,8 @@ abstract class _UpdateConfigParams implements UpdateConfigParams { _$UpdateConfigParamsImpl.fromJson; @override - @JsonKey(name: "profile-path") - String? get profilePath; + @JsonKey(name: "profile-id") + String get profileId; @override ClashConfig get config; @override @@ -1687,6 +1687,9 @@ ExternalProvider _$ExternalProviderFromJson(Map json) { mixin _$ExternalProvider { String get name => throw _privateConstructorUsedError; String get type => throw _privateConstructorUsedError; + String get path => throw _privateConstructorUsedError; + int get count => throw _privateConstructorUsedError; + bool get isUpdating => throw _privateConstructorUsedError; @JsonKey(name: "vehicle-type") String get vehicleType => throw _privateConstructorUsedError; @JsonKey(name: "update-at") @@ -1707,6 +1710,9 @@ abstract class $ExternalProviderCopyWith<$Res> { $Res call( {String name, String type, + String path, + int count, + bool isUpdating, @JsonKey(name: "vehicle-type") String vehicleType, @JsonKey(name: "update-at") DateTime updateAt}); } @@ -1726,6 +1732,9 @@ class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider> $Res call({ Object? name = null, Object? type = null, + Object? path = null, + Object? count = null, + Object? isUpdating = null, Object? vehicleType = null, Object? updateAt = null, }) { @@ -1738,6 +1747,18 @@ class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider> ? _value.type : type // ignore: cast_nullable_to_non_nullable as String, + path: null == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String, + count: null == count + ? _value.count + : count // ignore: cast_nullable_to_non_nullable + as int, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, vehicleType: null == vehicleType ? _value.vehicleType : vehicleType // ignore: cast_nullable_to_non_nullable @@ -1761,6 +1782,9 @@ abstract class _$$ExternalProviderImplCopyWith<$Res> $Res call( {String name, String type, + String path, + int count, + bool isUpdating, @JsonKey(name: "vehicle-type") String vehicleType, @JsonKey(name: "update-at") DateTime updateAt}); } @@ -1778,6 +1802,9 @@ class __$$ExternalProviderImplCopyWithImpl<$Res> $Res call({ Object? name = null, Object? type = null, + Object? path = null, + Object? count = null, + Object? isUpdating = null, Object? vehicleType = null, Object? updateAt = null, }) { @@ -1790,6 +1817,18 @@ class __$$ExternalProviderImplCopyWithImpl<$Res> ? _value.type : type // ignore: cast_nullable_to_non_nullable as String, + path: null == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String, + count: null == count + ? _value.count + : count // ignore: cast_nullable_to_non_nullable + as int, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, vehicleType: null == vehicleType ? _value.vehicleType : vehicleType // ignore: cast_nullable_to_non_nullable @@ -1808,6 +1847,9 @@ class _$ExternalProviderImpl implements _ExternalProvider { const _$ExternalProviderImpl( {required this.name, required this.type, + required this.path, + required this.count, + this.isUpdating = false, @JsonKey(name: "vehicle-type") required this.vehicleType, @JsonKey(name: "update-at") required this.updateAt}); @@ -1819,6 +1861,13 @@ class _$ExternalProviderImpl implements _ExternalProvider { @override final String type; @override + final String path; + @override + final int count; + @override + @JsonKey() + final bool isUpdating; + @override @JsonKey(name: "vehicle-type") final String vehicleType; @override @@ -1827,7 +1876,7 @@ class _$ExternalProviderImpl implements _ExternalProvider { @override String toString() { - return 'ExternalProvider(name: $name, type: $type, vehicleType: $vehicleType, updateAt: $updateAt)'; + return 'ExternalProvider(name: $name, type: $type, path: $path, count: $count, isUpdating: $isUpdating, vehicleType: $vehicleType, updateAt: $updateAt)'; } @override @@ -1837,6 +1886,10 @@ class _$ExternalProviderImpl implements _ExternalProvider { other is _$ExternalProviderImpl && (identical(other.name, name) || other.name == name) && (identical(other.type, type) || other.type == type) && + (identical(other.path, path) || other.path == path) && + (identical(other.count, count) || other.count == count) && + (identical(other.isUpdating, isUpdating) || + other.isUpdating == isUpdating) && (identical(other.vehicleType, vehicleType) || other.vehicleType == vehicleType) && (identical(other.updateAt, updateAt) || @@ -1845,8 +1898,8 @@ class _$ExternalProviderImpl implements _ExternalProvider { @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, name, type, vehicleType, updateAt); + int get hashCode => Object.hash( + runtimeType, name, type, path, count, isUpdating, vehicleType, updateAt); @JsonKey(ignore: true) @override @@ -1867,6 +1920,9 @@ abstract class _ExternalProvider implements ExternalProvider { const factory _ExternalProvider( {required final String name, required final String type, + required final String path, + required final int count, + final bool isUpdating, @JsonKey(name: "vehicle-type") required final String vehicleType, @JsonKey(name: "update-at") required final DateTime updateAt}) = _$ExternalProviderImpl; @@ -1879,6 +1935,12 @@ abstract class _ExternalProvider implements ExternalProvider { @override String get type; @override + String get path; + @override + int get count; + @override + bool get isUpdating; + @override @JsonKey(name: "vehicle-type") String get vehicleType; @override diff --git a/lib/models/generated/ffi.g.dart b/lib/models/generated/ffi.g.dart index ee6d37295..42670ecd6 100644 --- a/lib/models/generated/ffi.g.dart +++ b/lib/models/generated/ffi.g.dart @@ -27,7 +27,7 @@ Map _$$ConfigExtendedParamsImplToJson( _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson( Map json) => _$UpdateConfigParamsImpl( - profilePath: json['profile-path'] as String?, + profileId: json['profile-id'] as String, config: ClashConfig.fromJson(json['config'] as Map), params: ConfigExtendedParams.fromJson(json['params'] as Map), @@ -36,7 +36,7 @@ _$UpdateConfigParamsImpl _$$UpdateConfigParamsImplFromJson( Map _$$UpdateConfigParamsImplToJson( _$UpdateConfigParamsImpl instance) => { - 'profile-path': instance.profilePath, + 'profile-id': instance.profileId, 'config': instance.config, 'params': instance.params, }; @@ -156,6 +156,9 @@ _$ExternalProviderImpl _$$ExternalProviderImplFromJson( _$ExternalProviderImpl( name: json['name'] as String, type: json['type'] as String, + path: json['path'] as String, + count: (json['count'] as num).toInt(), + isUpdating: json['isUpdating'] as bool? ?? false, vehicleType: json['vehicle-type'] as String, updateAt: DateTime.parse(json['update-at'] as String), ); @@ -165,6 +168,9 @@ Map _$$ExternalProviderImplToJson( { 'name': instance.name, 'type': instance.type, + 'path': instance.path, + 'count': instance.count, + 'isUpdating': instance.isUpdating, 'vehicle-type': instance.vehicleType, 'update-at': instance.updateAt.toIso8601String(), }; diff --git a/lib/models/generated/profile.freezed.dart b/lib/models/generated/profile.freezed.dart index 7d45b1bbb..7263cc01d 100644 --- a/lib/models/generated/profile.freezed.dart +++ b/lib/models/generated/profile.freezed.dart @@ -223,6 +223,8 @@ mixin _$Profile { bool get autoUpdate => throw _privateConstructorUsedError; Map get selectedMap => throw _privateConstructorUsedError; Set get unfoldSet => throw _privateConstructorUsedError; + @JsonKey(includeToJson: false, includeFromJson: false) + bool get isUpdating => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -244,7 +246,8 @@ abstract class $ProfileCopyWith<$Res> { UserInfo? userInfo, bool autoUpdate, Map selectedMap, - Set unfoldSet}); + Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating}); $UserInfoCopyWith<$Res>? get userInfo; } @@ -272,6 +275,7 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> Object? autoUpdate = null, Object? selectedMap = null, Object? unfoldSet = null, + Object? isUpdating = null, }) { return _then(_value.copyWith( id: null == id @@ -314,6 +318,10 @@ class _$ProfileCopyWithImpl<$Res, $Val extends Profile> ? _value.unfoldSet : unfoldSet // ignore: cast_nullable_to_non_nullable as Set, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -347,7 +355,8 @@ abstract class _$$ProfileImplCopyWith<$Res> implements $ProfileCopyWith<$Res> { UserInfo? userInfo, bool autoUpdate, Map selectedMap, - Set unfoldSet}); + Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) bool isUpdating}); @override $UserInfoCopyWith<$Res>? get userInfo; @@ -374,6 +383,7 @@ class __$$ProfileImplCopyWithImpl<$Res> Object? autoUpdate = null, Object? selectedMap = null, Object? unfoldSet = null, + Object? isUpdating = null, }) { return _then(_$ProfileImpl( id: null == id @@ -416,6 +426,10 @@ class __$$ProfileImplCopyWithImpl<$Res> ? _value._unfoldSet : unfoldSet // ignore: cast_nullable_to_non_nullable as Set, + isUpdating: null == isUpdating + ? _value.isUpdating + : isUpdating // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -433,7 +447,9 @@ class _$ProfileImpl implements _Profile { this.userInfo, this.autoUpdate = true, final Map selectedMap = const {}, - final Set unfoldSet = const {}}) + final Set unfoldSet = const {}, + @JsonKey(includeToJson: false, includeFromJson: false) + this.isUpdating = false}) : _selectedMap = selectedMap, _unfoldSet = unfoldSet; @@ -476,9 +492,13 @@ class _$ProfileImpl implements _Profile { return EqualUnmodifiableSetView(_unfoldSet); } + @override + @JsonKey(includeToJson: false, includeFromJson: false) + final bool isUpdating; + @override String toString() { - return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet)'; + return 'Profile(id: $id, label: $label, currentGroupName: $currentGroupName, url: $url, lastUpdateDate: $lastUpdateDate, autoUpdateDuration: $autoUpdateDuration, userInfo: $userInfo, autoUpdate: $autoUpdate, selectedMap: $selectedMap, unfoldSet: $unfoldSet, isUpdating: $isUpdating)'; } @override @@ -502,7 +522,9 @@ class _$ProfileImpl implements _Profile { const DeepCollectionEquality() .equals(other._selectedMap, _selectedMap) && const DeepCollectionEquality() - .equals(other._unfoldSet, _unfoldSet)); + .equals(other._unfoldSet, _unfoldSet) && + (identical(other.isUpdating, isUpdating) || + other.isUpdating == isUpdating)); } @JsonKey(ignore: true) @@ -518,7 +540,8 @@ class _$ProfileImpl implements _Profile { userInfo, autoUpdate, const DeepCollectionEquality().hash(_selectedMap), - const DeepCollectionEquality().hash(_unfoldSet)); + const DeepCollectionEquality().hash(_unfoldSet), + isUpdating); @JsonKey(ignore: true) @override @@ -545,7 +568,9 @@ abstract class _Profile implements Profile { final UserInfo? userInfo, final bool autoUpdate, final Map selectedMap, - final Set unfoldSet}) = _$ProfileImpl; + final Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) + final bool isUpdating}) = _$ProfileImpl; factory _Profile.fromJson(Map json) = _$ProfileImpl.fromJson; @@ -570,6 +595,9 @@ abstract class _Profile implements Profile { @override Set get unfoldSet; @override + @JsonKey(includeToJson: false, includeFromJson: false) + bool get isUpdating; + @override @JsonKey(ignore: true) _$$ProfileImplCopyWith<_$ProfileImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 3e585d924..1405f1dae 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -2815,32 +2815,28 @@ abstract class _ProxiesListHeaderSelectorState } /// @nodoc -mixin _$CurrentGroupProxyNameSelectorState { - String? get proxyName => throw _privateConstructorUsedError; - String? get proxyName2 => throw _privateConstructorUsedError; +mixin _$ProxiesActionsState { + bool get isCurrent => throw _privateConstructorUsedError; + bool get hasProvider => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $CurrentGroupProxyNameSelectorStateCopyWith< - CurrentGroupProxyNameSelectorState> - get copyWith => throw _privateConstructorUsedError; + $ProxiesActionsStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { - factory $CurrentGroupProxyNameSelectorStateCopyWith( - CurrentGroupProxyNameSelectorState value, - $Res Function(CurrentGroupProxyNameSelectorState) then) = - _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, - CurrentGroupProxyNameSelectorState>; +abstract class $ProxiesActionsStateCopyWith<$Res> { + factory $ProxiesActionsStateCopyWith( + ProxiesActionsState value, $Res Function(ProxiesActionsState) then) = + _$ProxiesActionsStateCopyWithImpl<$Res, ProxiesActionsState>; @useResult - $Res call({String? proxyName, String? proxyName2}); + $Res call({bool isCurrent, bool hasProvider}); } /// @nodoc -class _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, - $Val extends CurrentGroupProxyNameSelectorState> - implements $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { - _$CurrentGroupProxyNameSelectorStateCopyWithImpl(this._value, this._then); +class _$ProxiesActionsStateCopyWithImpl<$Res, $Val extends ProxiesActionsState> + implements $ProxiesActionsStateCopyWith<$Res> { + _$ProxiesActionsStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -2850,117 +2846,109 @@ class _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? proxyName = freezed, - Object? proxyName2 = freezed, + Object? isCurrent = null, + Object? hasProvider = null, }) { 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?, + isCurrent: null == isCurrent + ? _value.isCurrent + : isCurrent // ignore: cast_nullable_to_non_nullable + as bool, + hasProvider: null == hasProvider + ? _value.hasProvider + : hasProvider // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } /// @nodoc -abstract class _$$CurrentGroupProxyNameSelectorStateImplCopyWith<$Res> - implements $CurrentGroupProxyNameSelectorStateCopyWith<$Res> { - factory _$$CurrentGroupProxyNameSelectorStateImplCopyWith( - _$CurrentGroupProxyNameSelectorStateImpl value, - $Res Function(_$CurrentGroupProxyNameSelectorStateImpl) then) = - __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl<$Res>; +abstract class _$$ProxiesActionsStateImplCopyWith<$Res> + implements $ProxiesActionsStateCopyWith<$Res> { + factory _$$ProxiesActionsStateImplCopyWith(_$ProxiesActionsStateImpl value, + $Res Function(_$ProxiesActionsStateImpl) then) = + __$$ProxiesActionsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({String? proxyName, String? proxyName2}); + $Res call({bool isCurrent, bool hasProvider}); } /// @nodoc -class __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl<$Res> - extends _$CurrentGroupProxyNameSelectorStateCopyWithImpl<$Res, - _$CurrentGroupProxyNameSelectorStateImpl> - implements _$$CurrentGroupProxyNameSelectorStateImplCopyWith<$Res> { - __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl( - _$CurrentGroupProxyNameSelectorStateImpl _value, - $Res Function(_$CurrentGroupProxyNameSelectorStateImpl) _then) +class __$$ProxiesActionsStateImplCopyWithImpl<$Res> + extends _$ProxiesActionsStateCopyWithImpl<$Res, _$ProxiesActionsStateImpl> + implements _$$ProxiesActionsStateImplCopyWith<$Res> { + __$$ProxiesActionsStateImplCopyWithImpl(_$ProxiesActionsStateImpl _value, + $Res Function(_$ProxiesActionsStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ - Object? proxyName = freezed, - Object? proxyName2 = freezed, + Object? isCurrent = null, + Object? hasProvider = null, }) { - 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?, + return _then(_$ProxiesActionsStateImpl( + isCurrent: null == isCurrent + ? _value.isCurrent + : isCurrent // ignore: cast_nullable_to_non_nullable + as bool, + hasProvider: null == hasProvider + ? _value.hasProvider + : hasProvider // ignore: cast_nullable_to_non_nullable + as bool, )); } } /// @nodoc -class _$CurrentGroupProxyNameSelectorStateImpl - implements _CurrentGroupProxyNameSelectorState { - const _$CurrentGroupProxyNameSelectorStateImpl( - {required this.proxyName, required this.proxyName2}); +class _$ProxiesActionsStateImpl implements _ProxiesActionsState { + const _$ProxiesActionsStateImpl( + {required this.isCurrent, required this.hasProvider}); @override - final String? proxyName; + final bool isCurrent; @override - final String? proxyName2; + final bool hasProvider; @override String toString() { - return 'CurrentGroupProxyNameSelectorState(proxyName: $proxyName, proxyName2: $proxyName2)'; + return 'ProxiesActionsState(isCurrent: $isCurrent, hasProvider: $hasProvider)'; } @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)); + other is _$ProxiesActionsStateImpl && + (identical(other.isCurrent, isCurrent) || + other.isCurrent == isCurrent) && + (identical(other.hasProvider, hasProvider) || + other.hasProvider == hasProvider)); } @override - int get hashCode => Object.hash(runtimeType, proxyName, proxyName2); + int get hashCode => Object.hash(runtimeType, isCurrent, hasProvider); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$CurrentGroupProxyNameSelectorStateImplCopyWith< - _$CurrentGroupProxyNameSelectorStateImpl> - get copyWith => __$$CurrentGroupProxyNameSelectorStateImplCopyWithImpl< - _$CurrentGroupProxyNameSelectorStateImpl>(this, _$identity); + _$$ProxiesActionsStateImplCopyWith<_$ProxiesActionsStateImpl> get copyWith => + __$$ProxiesActionsStateImplCopyWithImpl<_$ProxiesActionsStateImpl>( + this, _$identity); } -abstract class _CurrentGroupProxyNameSelectorState - implements CurrentGroupProxyNameSelectorState { - const factory _CurrentGroupProxyNameSelectorState( - {required final String? proxyName, - required final String? proxyName2}) = - _$CurrentGroupProxyNameSelectorStateImpl; +abstract class _ProxiesActionsState implements ProxiesActionsState { + const factory _ProxiesActionsState( + {required final bool isCurrent, + required final bool hasProvider}) = _$ProxiesActionsStateImpl; @override - String? get proxyName; + bool get isCurrent; @override - String? get proxyName2; + bool get hasProvider; @override @JsonKey(ignore: true) - _$$CurrentGroupProxyNameSelectorStateImplCopyWith< - _$CurrentGroupProxyNameSelectorStateImpl> - get copyWith => throw _privateConstructorUsedError; + _$$ProxiesActionsStateImplCopyWith<_$ProxiesActionsStateImpl> get copyWith => + throw _privateConstructorUsedError; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 1825f4cdc..b7439b840 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,3 +1,4 @@ +// ignore_for_file: invalid_annotation_target import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -55,6 +56,9 @@ class Profile with _$Profile { @Default(true) bool autoUpdate, @Default({}) SelectedMap selectedMap, @Default({}) Set unfoldSet, + @JsonKey(includeToJson: false, includeFromJson: false) + @Default(false) + bool isUpdating, }) = _Profile; factory Profile.fromJson(Map json) => @@ -63,7 +67,7 @@ class Profile with _$Profile { factory Profile.normal({ String? label, String url = '', -}) { + }) { return Profile( label: label, url: url, @@ -77,8 +81,7 @@ extension ProfileExtension on Profile { ProfileType get type => url.isEmpty == true ? ProfileType.file : ProfileType.url; - bool get realAutoUpdate => - url.isEmpty == true ? false : autoUpdate; + bool get realAutoUpdate => url.isEmpty == true ? false : autoUpdate; Future checkAndUpdate() async { final isExists = await check(); diff --git a/lib/models/proxy.dart b/lib/models/proxy.dart index e122454de..c3cb7cbec 100644 --- a/lib/models/proxy.dart +++ b/lib/models/proxy.dart @@ -41,4 +41,4 @@ class Proxy with _$Proxy { }) = _Proxy; factory Proxy.fromJson(Map json) => _$ProxyFromJson(json); -} +} \ No newline at end of file diff --git a/lib/models/selector.dart b/lib/models/selector.dart index addac1248..0d18aaba0 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -137,7 +137,6 @@ class PackageListSelectorState with _$PackageListSelectorState { }) = _PackageListSelectorState; } - @freezed class ColumnsSelectorState with _$ColumnsSelectorState { const factory ColumnsSelectorState({ @@ -154,10 +153,11 @@ class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState { }) = _ProxiesListHeaderSelectorState; } + @freezed -class CurrentGroupProxyNameSelectorState with _$CurrentGroupProxyNameSelectorState { - const factory CurrentGroupProxyNameSelectorState({ - required String? proxyName, - required String? proxyName2, - }) = _CurrentGroupProxyNameSelectorState; +class ProxiesActionsState with _$ProxiesActionsState { + const factory ProxiesActionsState({ + required bool isCurrent, + required bool hasProvider, + }) = _ProxiesActionsState; } \ No newline at end of file diff --git a/lib/state.dart b/lib/state.dart index 6bf9fbc73..351fc9f77 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -45,11 +45,10 @@ class GlobalState { required Config config, bool isPatch = true, }) async { - final profilePath = await appPath.getProfilePath(config.currentProfileId); await config.currentProfile?.checkAndUpdate(); final res = await clashCore.updateConfig( UpdateConfigParams( - profilePath: profilePath, + profileId: config.currentProfileId ?? "", config: clashConfig, params: ConfigExtendedParams( isPatch: isPatch, @@ -96,8 +95,12 @@ class GlobalState { config: config, isPatch: false, ); - clashCore.setProfileName(config.currentProfile?.label ?? ''); await updateGroups(appState); + await updateProviders(appState); + } + + updateProviders(AppState appState) async { + appState.providers = await clashCore.getExternalProviders(); } init({ @@ -118,6 +121,7 @@ class GlobalState { systemProxy: config.systemProxy, mixedPort: clashConfig.mixedPort, onlyProxy: config.onlyProxy, + currentProfileName: config.currentProfile?.label ?? config.currentProfileId ?? "", ), ); } @@ -204,7 +208,7 @@ class GlobalState { final traffic = clashCore.getTraffic(); if (Platform.isAndroid && isVpnService == true) { proxy?.startForeground( - title: clashCore.getProfileName(), + title: clashCore.getState().currentProfileName, content: "$traffic", ); } else { diff --git a/lib/widgets/builder.dart b/lib/widgets/builder.dart index 829439d98..7a1710f51 100644 --- a/lib/widgets/builder.dart +++ b/lib/widgets/builder.dart @@ -1,4 +1,6 @@ +import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class ScrollOverBuilder extends StatefulWidget { final Widget Function(bool isOver) builder; @@ -15,7 +17,6 @@ class ScrollOverBuilder extends StatefulWidget { class _ScrollOverBuilderState extends State { final isOverNotifier = ValueNotifier(false); - @override void dispose() { super.dispose(); @@ -38,3 +39,29 @@ class _ScrollOverBuilderState extends State { ); } } + +class ProxiesActionsBuilder extends StatelessWidget { + final Widget? child; + final Widget Function( + ProxiesActionsState state, + Widget? child, + ) builder; + + const ProxiesActionsBuilder({ + super.key, + required this.child, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => ProxiesActionsState( + isCurrent: appState.currentLabel == "proxies", + hasProvider: appState.providers.isNotEmpty, + ), + builder: (_, state, child) => builder(state, child), + child: child, + ); + } +} diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index a743251a8..19920643f 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -32,40 +32,39 @@ class InfoHeader extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (info.iconData != null) ...[ - Icon( - info.iconData, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox( - width: 8, - ), - ], - Flexible( - child: TooltipText( - text: Text( - info.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ), - ], - ), Expanded( - flex: 1, child: Row( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, children: [ - ...actions, + if (info.iconData != null) ...[ + Icon( + info.iconData, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox( + width: 8, + ), + ], + Flexible( + child: TooltipText( + text: Text( + info.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), ], ), ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ...actions, + ], + ), ], ), ); @@ -146,13 +145,11 @@ class CommonCard extends StatelessWidget { childWidget = Column( mainAxisSize: MainAxisSize.min, children: [ - Flexible( - flex: 0, - child: InfoHeader( - info: info!, - ), + InfoHeader( + info: info!, ), Flexible( + flex: 1, child: child, ), ], diff --git a/lib/widgets/clash_container.dart b/lib/widgets/clash_container.dart index c3f7aa740..e38736b91 100644 --- a/lib/widgets/clash_container.dart +++ b/lib/widgets/clash_container.dart @@ -27,6 +27,8 @@ class _ClashContainerState extends State systemProxy: config.systemProxy, mixedPort: clashConfig.mixedPort, onlyProxy: config.onlyProxy, + currentProfileName: + config.currentProfile?.label ?? config.currentProfileId ?? "", ), builder: (__, state, child) { clashCore.setState(state); @@ -36,9 +38,32 @@ class _ClashContainerState extends State ); } + _changeProfileHandle() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final appController = globalState.appController; + appController.appState.delayMap = {}; + await appController.applyProfile(); + }); + } + + Widget _changeProfileContainer(Widget child) { + return Selector( + selector: (_, config) => config.currentProfileId, + builder: (__, state, child) { + _changeProfileHandle(); + return child!; + }, + child: child, + ); + } + @override Widget build(BuildContext context) { - return _updateCoreState(widget.child); + return _changeProfileContainer( + _updateCoreState( + widget.child, + ), + ); } @override @@ -73,16 +98,15 @@ class _ClashContainerState extends State } @override - void onLoaded(String groupName) { + void onLoaded(String providerName) { final appController = globalState.appController; - final currentSelectedMap = appController.config.currentSelectedMap; - final proxyName = currentSelectedMap[groupName]; - if (proxyName == null) return; - appController.changeProxy( - groupName: groupName, - proxyName: proxyName, + appController.appState.setProvider( + clashCore.getExternalProvider( + providerName, + ), ); - super.onLoaded(proxyName); + appController.addCheckIpNumDebounce(); + super.onLoaded(providerName); } @override diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index d21b99ede..2eccab37a 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -5,10 +5,12 @@ import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; import 'side_sheet.dart'; -showExtendPage(BuildContext context, { +showExtendPage( + BuildContext context, { required Widget body, required String title, double? extendPageWidth, + bool forceNotSide = false, Widget? action, }) { final NavigatorState navigator = Navigator.of(context); @@ -17,23 +19,24 @@ showExtendPage(BuildContext context, { key: globalKey, child: body, ); - final isMobile = globalState.appController.appState.viewMode == - ViewMode.mobile; + final isMobile = + globalState.appController.appState.viewMode == ViewMode.mobile; + final isNotSide = isMobile || forceNotSide; navigator.push( ModalSideSheetRoute( modalBarrierColor: Colors.black38, builder: (context) { final commonScaffold = CommonScaffold( - automaticallyImplyLeading: isMobile ? true : false, - actions: isMobile + automaticallyImplyLeading: isNotSide, + actions: isNotSide ? null : [ - const SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: CloseButton(), - ), - ], + const SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: CloseButton(), + ), + ], title: title, body: uniqueBody, ); diff --git a/lib/widgets/window_container.dart b/lib/widgets/window_container.dart index 09cdd1183..008b01d11 100644 --- a/lib/widgets/window_container.dart +++ b/lib/widgets/window_container.dart @@ -220,16 +220,14 @@ class _WindowHeaderState extends State { ), ), ), - if (!Platform.isMacOS) ...[ - const Positioned( - left: 0, - child: AppIcon(), - ), - Positioned( - right: 0, - child: _buildActions(), - ), - ] + const Positioned( + left: 0, + child: AppIcon(), + ), + Positioned( + right: 0, + child: _buildActions(), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 8e734a704..377510822 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,7 +34,7 @@ packages: source: hosted version: "3.5.1" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d @@ -525,6 +525,22 @@ 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: @@ -828,6 +844,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + re_editor: + dependency: "direct main" + description: + name: re_editor + sha256: abae2b015799c936b9f9b68888e2c55007dd159b4654a85da22ce1af84efbd17 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + 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 1cd0df386..7bcc9125c 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.50+202408011 +version: 0.8.51+202408051 environment: sdk: '>=3.1.0 <4.0.0' @@ -41,6 +41,9 @@ dependencies: win32: ^5.5.1 ffi: ^2.1.2 material_color_utilities: ^0.8.0 + re_editor: ^0.3.1 + re_highlight: ^0.0.3 + archive: ^3.6.1 dev_dependencies: flutter_test: sdk: flutter From e3c903590366125dc9af11101e73abb215c5826f Mon Sep 17 00:00:00 2001 From: chen08209 Date: Mon, 5 Aug 2024 19:25:35 +0800 Subject: [PATCH 09/25] Update access control page Fix bug --- .github/workflows/build.yml | 9 +- android/app/build.gradle | 3 + .../kotlin/com/follow/clash/models/Package.kt | 5 +- .../com/follow/clash/plugins/AppPlugin.kt | 177 +++- core/Clash.Meta | 2 +- core/common.go | 82 +- core/hub.go | 184 ++-- lib/application.dart | 14 - lib/clash/core.dart | 58 +- lib/clash/generated/clash_ffi.dart | 47 +- lib/common/constant.dart | 1 - lib/common/dav_client.dart | 6 - lib/common/other.dart | 11 +- lib/common/request.dart | 4 +- lib/common/string.dart | 6 + lib/controller.dart | 6 +- lib/enum/enum.dart | 2 + lib/fragments/access.dart | 820 +++++++++++------- lib/fragments/backup_and_recovery.dart | 5 +- .../dashboard/network_detection.dart | 1 - lib/fragments/profiles/edit_profile.dart | 2 - lib/fragments/profiles/profiles.dart | 2 +- lib/fragments/proxies/common.dart | 1 - lib/fragments/proxies/providers.dart | 71 +- lib/fragments/proxies/setting.dart | 72 +- lib/fragments/proxies/tab.dart | 1 - lib/fragments/resources.dart | 6 +- lib/l10n/arb/intl_en.arb | 11 +- lib/l10n/arb/intl_zh_CN.arb | 11 +- lib/l10n/intl/messages_en.dart | 13 + lib/l10n/intl/messages_zh_CN.dart | 9 + lib/l10n/l10n.dart | 90 ++ lib/main.dart | 8 +- lib/models/app.dart | 3 +- lib/models/config.dart | 8 + lib/models/generated/config.freezed.dart | 24 +- lib/models/generated/config.g.dart | 9 + lib/models/generated/package.freezed.dart | 38 +- lib/models/generated/package.g.dart | 2 + lib/models/generated/selector.freezed.dart | 48 +- lib/models/package.dart | 1 + lib/models/selector.dart | 38 +- lib/plugins/app.dart | 10 + lib/plugins/proxy.dart | 1 - lib/widgets/clash_container.dart | 3 +- lib/widgets/setting.dart | 74 ++ lib/widgets/widgets.dart | 3 +- pubspec.lock | 16 + pubspec.yaml | 3 +- test/command_test.dart | 13 +- 50 files changed, 1391 insertions(+), 643 deletions(-) create mode 100644 lib/widgets/setting.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1803f84c6..2483099b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,18 +136,25 @@ jobs: gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog" echo -e "\n\n" >> release.md fi + - name: Release uses: softprops/action-gh-release@v2 with: files: ./dist/* body_path: './release.md' + - name: Create Fdroid Source Dir + run: | + mkdir -p ./tmp + cp ./dist/*android-arm64-v8a* ./tmp/ || true + echo "Files copied successfully" + - name: Push to fdroid repo uses: cpina/github-action-push-to-another-repository@v1.7.2 env: SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} with: - source-directory: ./dist/ + source-directory: ./tmp/ destination-github-username: chen08209 destination-repository-name: FlClash-fdroid-repo user-name: 'github-actions[bot]' diff --git a/android/app/build.gradle b/android/app/build.gradle index c86cd997c..b3e1db131 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -102,6 +102,9 @@ flutter { dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'com.google.code.gson:gson:2.10' + implementation("com.android.tools.smali:smali-dexlib2:3.0.7") { + exclude group: "com.google.guava", module: "guava" + } } diff --git a/android/app/src/main/kotlin/com/follow/clash/models/Package.kt b/android/app/src/main/kotlin/com/follow/clash/models/Package.kt index 41c87315f..7240c3b90 100644 --- a/android/app/src/main/kotlin/com/follow/clash/models/Package.kt +++ b/android/app/src/main/kotlin/com/follow/clash/models/Package.kt @@ -1,7 +1,10 @@ package com.follow.clash.models +import java.util.Date + data class Package( val packageName: String, val label: String, - val isSystem:Boolean + val isSystem: Boolean, + val firstInstallTime: Long, ) diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt index 809019dcc..949785310 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 @@ -6,11 +6,13 @@ import android.app.ActivityManager import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo +import android.content.pm.ComponentInfo import android.content.pm.PackageManager import android.net.ConnectivityManager import android.os.Build import android.widget.Toast import androidx.core.content.ContextCompat.getSystemService +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile import androidx.core.content.FileProvider import androidx.core.content.getSystemService import com.follow.clash.GlobalState @@ -32,6 +34,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.net.InetSocketAddress +import java.util.zip.ZipFile class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { @@ -49,6 +52,62 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware private var connectivity: ConnectivityManager? = null private val iconMap = mutableMapOf() + private val packages = mutableListOf() + + private val skipPrefixList = listOf( + "com.google", + "com.android.chrome", + "com.android.vending", + "com.microsoft", + "com.apple", + "com.zhiliaoapp.musically", // Banned by China + ) + + private val chinaAppPrefixList = listOf( + "com.tencent", + "com.alibaba", + "com.umeng", + "com.qihoo", + "com.ali", + "com.alipay", + "com.amap", + "com.sina", + "com.weibo", + "com.vivo", + "com.xiaomi", + "com.huawei", + "com.taobao", + "com.secneo", + "s.h.e.l.l", + "com.stub", + "com.kiwisec", + "com.secshell", + "com.wrapper", + "cn.securitystack", + "com.mogosec", + "com.secoen", + "com.netease", + "com.mx", + "com.qq.e", + "com.baidu", + "com.bytedance", + "com.bugly", + "com.miui", + "com.oppo", + "com.coloros", + "com.iqoo", + "com.meizu", + "com.gionee", + "cn.nubia", + "com.oplus", + "andes.oplus", + "com.unionpay", + "cn.wps" + ) + + private val chinaAppRegex by lazy { + ("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex() + } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { scope = CoroutineScope(Dispatchers.Default) @@ -88,7 +147,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware "getPackages" -> { scope.launch { - result.success(getPackages()) + result.success(getPackagesToJson()) + } + } + + "getChinaPackageNames" -> { + scope.launch { + result.success(getChinaPackageNames()) } } @@ -248,26 +313,106 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware return iconMap[packageName] } - private suspend fun getPackages(): String { + private fun getPackages(): List { + val packageManager = context?.packageManager + if (packages.isNotEmpty()) return packages; + packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { + it.packageName != context?.packageName + || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + || it.packageName == "android" + + }?.map { + Package( + packageName = it.packageName, + label = it.applicationInfo.loadLabel(packageManager).toString(), + isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, + firstInstallTime = it.firstInstallTime + ) + }?.let { packages.addAll(it) } + return packages; + } + + private suspend fun getPackagesToJson(): String { return withContext(Dispatchers.Default) { - val packageManager = context?.packageManager - val packages: List? = - packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { - it.packageName != context?.packageName - || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true - || it.packageName == "android" - - }?.map { - Package( - packageName = it.packageName, - label = it.applicationInfo.loadLabel(packageManager).toString(), - isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1 - ) - } + Gson().toJson(getPackages()) + } + } + + private suspend fun getChinaPackageNames(): String { + return withContext(Dispatchers.Default) { + val packages: List = + getPackages().map { it.packageName }.filter { isChinaPackage(it) } Gson().toJson(packages) } } + private fun isChinaPackage(packageName: String): Boolean { + val packageManager = context?.packageManager ?: return false + skipPrefixList.forEach { + if (packageName == it || packageName.startsWith("$it.")) return false + } + val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS + } else { + @Suppress("DEPRECATION") + PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS + } + if (packageName.matches(chinaAppRegex)) { + return true + } + try { + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong()) + ) + } else { + @Suppress("DEPRECATION") packageManager.getPackageInfo( + packageName, packageManagerFlags + ) + } + mutableListOf().apply { + packageInfo.services?.let { addAll(it) } + packageInfo.activities?.let { addAll(it) } + packageInfo.receivers?.let { addAll(it) } + packageInfo.providers?.let { addAll(it) } + }.forEach { + if (it.name.matches(chinaAppRegex)) return true + } + ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use { + for (packageEntry in it.entries()) { + if (packageEntry.name.startsWith("firebase-")) return false + } + for (packageEntry in it.entries()) { + if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith( + ".dex" + )) + ) { + continue + } + if (packageEntry.size > 15000000) { + return true + } + val input = it.getInputStream(packageEntry).buffered() + val dexFile = try { + DexBackedDexFile.fromInputStream(null, input) + } catch (e: Exception) { + return false + } + for (clazz in dexFile.classes) { + val clazzName = + clazz.type.substring(1, clazz.type.length - 1).replace("/", ".") + .replace("$", ".") + if (clazzName.matches(chinaAppRegex)) return true + } + } + } + } catch (_: Exception) { + return false + } + return false + } + fun requestGc() { channel.invokeMethod("gc", null) } diff --git a/core/Clash.Meta b/core/Clash.Meta index fffdf8449..44d4b6dab 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit fffdf84493f054423b23e6883bcc2cdcfe877439 +Subproject commit 44d4b6dab23ec596f5df9acc7fadb66f4eb30bd0 diff --git a/core/common.go b/core/common.go index 45056e0fc..0a82f49de 100644 --- a/core/common.go +++ b/core/common.go @@ -3,6 +3,7 @@ package main import "C" import ( "context" + "errors" "github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outboundgroup" @@ -102,6 +103,12 @@ type ExternalProvider struct { UpdateAt time.Time `json:"update-at"` } +type ExternalProviders []ExternalProvider + +func (a ExternalProviders) Len() int { return len(a) } +func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name } +func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) func restartExecutable(execPath string) { @@ -190,35 +197,67 @@ func getRawConfigWithId(id string) *config.RawConfig { return prof } -func getExternalProvidersRaw() map[string]ExternalProvider { - externalProviders := make(map[string]ExternalProvider) +func getExternalProvidersRaw() map[string]cp.Provider { + eps := make(map[string]cp.Provider) for n, p := range tunnel.Providers() { if p.VehicleType() != cp.Compatible { - p := p.(*provider.ProxySetProvider) - externalProviders[n] = ExternalProvider{ - Name: n, - Type: p.Type().String(), - VehicleType: p.VehicleType().String(), - Count: p.Count(), - Path: p.Vehicle().Path(), - UpdateAt: p.UpdatedAt, - } + eps[n] = p } } for n, p := range tunnel.RuleProviders() { if p.VehicleType() != cp.Compatible { - p := p.(*rp.RuleSetProvider) - externalProviders[n] = ExternalProvider{ - Name: n, - Type: p.Type().String(), - VehicleType: p.VehicleType().String(), - Count: p.Count(), - Path: p.Vehicle().Path(), - UpdateAt: p.UpdatedAt, - } + eps[n] = p + } + } + return eps +} + +func toExternalProvider(p cp.Provider) (*ExternalProvider, error) { + switch p.(type) { + case *provider.ProxySetProvider: + psp := p.(*provider.ProxySetProvider) + return &ExternalProvider{ + Name: psp.Name(), + Type: psp.Type().String(), + VehicleType: psp.VehicleType().String(), + Count: psp.Count(), + Path: psp.Vehicle().Path(), + UpdateAt: psp.UpdatedAt, + }, nil + case *rp.RuleSetProvider: + rsp := p.(*rp.RuleSetProvider) + return &ExternalProvider{ + Name: rsp.Name(), + Type: rsp.Type().String(), + VehicleType: rsp.VehicleType().String(), + Count: rsp.Count(), + Path: rsp.Vehicle().Path(), + UpdateAt: rsp.UpdatedAt, + }, nil + default: + return nil, errors.New("not external provider") + } +} + +func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error { + switch p.(type) { + case *provider.ProxySetProvider: + psp := p.(*provider.ProxySetProvider) + elm, same, err := psp.SideUpdate(bytes) + if err == nil && !same { + psp.OnUpdate(elm) + } + return nil + case rp.RuleSetProvider: + rsp := p.(*rp.RuleSetProvider) + elm, same, err := rsp.SideUpdate(bytes) + if err == nil && !same { + rsp.OnUpdate(elm) } + return nil + default: + return errors.New("not external provider") } - return externalProviders } func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig { @@ -487,5 +526,6 @@ func applyConfig() error { hub.UltraApplyConfig(cfg, true) patchSelectGroup() } + externalProviders = getExternalProvidersRaw() return err } diff --git a/core/hub.go b/core/hub.go index 355b0d776..8ad4fc6d6 100644 --- a/core/hub.go +++ b/core/hub.go @@ -18,12 +18,12 @@ import ( cp "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/log" - rp "github.com/metacubex/mihomo/rules/provider" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel/statistic" "golang.org/x/net/context" "os" "runtime" + "sort" "time" "unsafe" ) @@ -32,6 +32,8 @@ var currentConfig = config.DefaultRawConfig() var configParams = ConfigExtendedParams{} +var externalProviders = map[string]cp.Provider{} + var isInit = false //export initClash @@ -311,34 +313,16 @@ func getProvider(name *C.char) *C.char { //export getExternalProviders func getExternalProviders() *C.char { - externalProviders := make(map[string]ExternalProvider) - for n, p := range tunnel.Providers() { - if p.VehicleType() != cp.Compatible { - p := p.(*provider.ProxySetProvider) - externalProviders[n] = ExternalProvider{ - Name: n, - Type: p.Type().String(), - VehicleType: p.VehicleType().String(), - Count: p.Count(), - Path: p.Vehicle().Path(), - UpdateAt: p.UpdatedAt, - } - } - } - for n, p := range tunnel.RuleProviders() { - if p.VehicleType() != cp.Compatible { - p := p.(*rp.RuleSetProvider) - externalProviders[n] = ExternalProvider{ - Name: n, - Type: p.Type().String(), - VehicleType: p.VehicleType().String(), - Count: p.Count(), - Path: p.Vehicle().Path(), - UpdateAt: p.UpdatedAt, - } + eps := make([]ExternalProvider, 0) + for _, p := range externalProviders { + externalProvider, err := toExternalProvider(p) + if err != nil { + continue } + eps = append(eps, *externalProvider) } - data, err := json.Marshal(externalProviders) + sort.Sort(ExternalProviders(eps)) + data, err := json.Marshal(eps) if err != nil { return C.CString("") } @@ -348,69 +332,48 @@ func getExternalProviders() *C.char { //export getExternalProvider func getExternalProvider(name *C.char) *C.char { externalProviderName := C.GoString(name) - externalProviders := getExternalProvidersRaw() externalProvider, exist := externalProviders[externalProviderName] if !exist { return C.CString("") } - data, err := json.Marshal(externalProvider) + e, err := toExternalProvider(externalProvider) + if err != nil { + return C.CString("") + } + data, err := json.Marshal(e) if err != nil { return C.CString("") } return C.CString(string(data)) } -//export updateExternalProvider -func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) { +//export updateGeoData +func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) { i := int64(port) - providerNameString := C.GoString(providerName) - providerTypeString := C.GoString(providerType) + geoTypeString := C.GoString(geoType) + geoNameString := C.GoString(geoName) go func() { - switch providerTypeString { - case "Proxy": - providers := tunnel.Providers() - proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) - if !exist { - bridge.SendToPort(i, "proxy provider is not exist") - return - } - err := proxyProvider.Update() - if err != nil { - bridge.SendToPort(i, err.Error()) - return - } - case "Rule": - providers := tunnel.RuleProviders() - ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) - if !exist { - bridge.SendToPort(i, "rule provider is not exist") - return - } - err := ruleProvider.Update() - if err != nil { - bridge.SendToPort(i, err.Error()) - return - } + switch geoTypeString { case "MMDB": - err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) + err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return } case "ASN": - err := updater.UpdateASN(constant.Path.Resolve(providerNameString)) + err := updater.UpdateASN(constant.Path.Resolve(geoNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return } case "GeoIp": - err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString)) + err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return } case "GeoSite": - err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString)) + err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString)) if err != nil { bridge.SendToPort(i, err.Error()) return @@ -420,65 +383,44 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l }() } -//func sideLoadExternalProvider(providerName *C.char, providerType *C.char, data *C.char, port C.longlong) { -// i := int64(port) -// bytes := []byte(C.GoString(data)) -// providerNameString := C.GoString(providerName) -// providerTypeString := C.GoString(providerType) -// go func() { -// switch providerTypeString { -// case "Proxy": -// providers := tunnel.Providers() -// proxyProvider, exist := providers[providerNameString].(*provider.ProxySetProvider) -// if exist { -// bridge.SendToPort(i, "proxy provider is not exist") -// return -// } -// err := proxyProvider.Update() -// if err != nil { -// bridge.SendToPort(i, err.Error()) -// return -// } -// case "Rule": -// providers := tunnel.RuleProviders() -// ruleProvider, exist := providers[providerNameString].(*rp.RuleSetProvider) -// if exist { -// bridge.SendToPort(i, "proxy provider is not exist") -// return -// } -// err := ruleProvider.Update() -// if err != nil { -// bridge.SendToPort(i, err.Error()) -// return -// } -// case "MMDB": -// err := updater.UpdateMMDB(constant.Path.Resolve(providerNameString)) -// if err != nil { -// bridge.SendToPort(i, err.Error()) -// return -// } -// case "ASN": -// err := updater.UpdateASN(constant.Path.Resolve(providerNameString)) -// if err != nil { -// bridge.SendToPort(i, err.Error()) -// return -// } -// case "GeoIp": -// err := updater.UpdateGeoIp(constant.Path.Resolve(providerNameString)) -// if err != nil { -// bridge.SendToPort(i, err.Error()) -// return -// } -// case "GeoSite": -// err := updater.UpdateGeoSite(constant.Path.Resolve(providerNameString)) -// if err != nil { -// bridge.SendToPort(i, err.Error()) -// return -// } -// } -// bridge.SendToPort(i, "") -// }() -//} +//export updateExternalProvider +func updateExternalProvider(providerName *C.char, port C.longlong) { + i := int64(port) + providerNameString := C.GoString(providerName) + go func() { + externalProvider, exist := externalProviders[providerNameString] + if !exist { + bridge.SendToPort(i, "external provider is not exist") + return + } + err := externalProvider.Update() + if err != nil { + bridge.SendToPort(i, err.Error()) + return + } + bridge.SendToPort(i, "") + }() +} + +//export sideLoadExternalProvider +func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) { + i := int64(port) + bytes := []byte(C.GoString(data)) + providerNameString := C.GoString(providerName) + go func() { + externalProvider, exist := externalProviders[providerNameString] + if !exist { + bridge.SendToPort(i, "external provider is not exist") + return + } + err := sideUpdateExternalProvider(externalProvider, bytes) + if err != nil { + bridge.SendToPort(i, err.Error()) + return + } + bridge.SendToPort(i, "") + }() +} //export initNativeApiBridge func initNativeApiBridge(api unsafe.Pointer) { diff --git a/lib/application.dart b/lib/application.dart index d3288047a..81cbcf657 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -88,7 +88,6 @@ class ApplicationState extends State { } await globalState.appController.init(); globalState.appController.initLink(); - _updateGroups(); }); } @@ -120,19 +119,6 @@ class ApplicationState extends State { }); } - _updateGroups() { - if (globalState.groupsUpdateTimer != null) { - globalState.groupsUpdateTimer?.cancel(); - globalState.groupsUpdateTimer = null; - } - globalState.groupsUpdateTimer ??= Timer.periodic( - httpTimeoutDuration, - (timer) async { - await globalState.appController.updateGroupDebounce(); - }, - ); - } - @override Widget build(context) { return AppStateContainer( diff --git a/lib/clash/core.dart b/lib/clash/core.dart index f518d46f4..8f5add83c 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -140,8 +140,7 @@ class ClashCore { clashFFI.freeCString(externalProvidersRaw); return Isolate.run>(() { final externalProviders = - (json.decode(externalProvidersRawString) as Map) - .values + (json.decode(externalProvidersRawString) as List) .map( (item) => ExternalProvider.fromJson(item), ) @@ -150,7 +149,7 @@ class ClashCore { }); } - ExternalProvider getExternalProvider(String externalProviderName) { + ExternalProvider? getExternalProvider(String externalProviderName) { final externalProviderNameChar = externalProviderName.toNativeUtf8().cast(); final externalProviderRaw = @@ -159,12 +158,60 @@ class ClashCore { final externalProviderRawString = externalProviderRaw.cast().toDartString(); clashFFI.freeCString(externalProviderRaw); + if(externalProviderRawString.isEmpty) return null; return ExternalProvider.fromJson(json.decode(externalProviderRawString)); } + Future updateGeoData({ + required String geoType, + required String geoName, + }) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final geoTypeChar = geoType.toNativeUtf8().cast(); + final geoNameChar = geoName.toNativeUtf8().cast(); + clashFFI.updateGeoData( + geoTypeChar, + geoNameChar, + receiver.sendPort.nativePort, + ); + malloc.free(geoTypeChar); + malloc.free(geoNameChar); + return completer.future; + } + + Future sideLoadExternalProvider({ + required String providerName, + required String data, + }) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final providerNameChar = providerName.toNativeUtf8().cast(); + final dataChar = data.toNativeUtf8().cast(); + clashFFI.sideLoadExternalProvider( + providerNameChar, + dataChar, + receiver.sendPort.nativePort, + ); + malloc.free(providerNameChar); + malloc.free(dataChar); + return completer.future; + } + Future updateExternalProvider({ required String providerName, - required String providerType, }) { final completer = Completer(); final receiver = ReceivePort(); @@ -175,14 +222,11 @@ class ClashCore { } }); final providerNameChar = providerName.toNativeUtf8().cast(); - final providerTypeChar = providerType.toNativeUtf8().cast(); clashFFI.updateExternalProvider( providerNameChar, - providerTypeChar, receiver.sendPort.nativePort, ); malloc.free(providerNameChar); - malloc.free(providerTypeChar); return completer.future; } diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index e081ea55a..366dc766e 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -5400,24 +5400,61 @@ class ClashFFI { late final _getExternalProvider = _getExternalProviderPtr .asFunction Function(ffi.Pointer)>(); + void updateGeoData( + ffi.Pointer geoType, + ffi.Pointer geoName, + int port, + ) { + return _updateGeoData( + geoType, + geoName, + port, + ); + } + + late final _updateGeoDataPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.LongLong)>>('updateGeoData'); + late final _updateGeoData = _updateGeoDataPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int)>(); + void updateExternalProvider( ffi.Pointer providerName, - ffi.Pointer providerType, int port, ) { return _updateExternalProvider( providerName, - providerType, port, ); } late final _updateExternalProviderPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.LongLong)>>('updateExternalProvider'); + late final _updateExternalProvider = _updateExternalProviderPtr + .asFunction, int)>(); + + void sideLoadExternalProvider( + ffi.Pointer providerName, + ffi.Pointer data, + int port, + ) { + return _sideLoadExternalProvider( + providerName, + data, + port, + ); + } + + late final _sideLoadExternalProviderPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.LongLong)>>('updateExternalProvider'); - late final _updateExternalProvider = _updateExternalProviderPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int)>(); + ffi.LongLong)>>('sideLoadExternalProvider'); + late final _sideLoadExternalProvider = + _sideLoadExternalProviderPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int)>(); void initNativeApiBridge( ffi.Pointer api, diff --git a/lib/common/constant.dart b/lib/common/constant.dart index a79a283a0..d9ce9c633 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:ui'; import 'package:fl_clash/enum/enum.dart'; diff --git a/lib/common/dav_client.dart b/lib/common/dav_client.dart index 905624391..3b0129860 100644 --- a/lib/common/dav_client.dart +++ b/lib/common/dav_client.dart @@ -1,12 +1,8 @@ import 'dart:async'; -import 'dart:convert'; 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/state.dart'; -import 'package:path/path.dart'; import 'package:webdav_client/webdav_client.dart'; class DAVClient { @@ -34,8 +30,6 @@ class DAVClient { Future _ping() async { try { await client.ping(); - await client.mkdir("/$appName"); - await client.mkdir("/$appName/$profilesDirectoryName"); return true; } catch (_) { return false; diff --git a/lib/common/other.dart b/lib/common/other.dart index f9746e6cd..667cbb535 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -2,11 +2,10 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; -import 'package:fl_clash/common/app_localizations.dart'; import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; +import 'package:lpinyin/lpinyin.dart'; import 'package:zxing2/qrcode.dart'; import 'package:image/image.dart' as img; @@ -84,7 +83,7 @@ class Other { if (charA == charB) { return sortByChar(a.substring(1), b.substring(1)); } else { - return charA.compareTo(charB); + return charA.compareToLower(charB); } } @@ -200,8 +199,8 @@ class Other { return targetColumnsArray.first; } - String getColumnsTextForInt(int number){ - return switch(number){ + String getColumnsTextForInt(int number) { + return switch (number) { 1 => appLocalizations.oneColumn, 2 => appLocalizations.twoColumns, 3 => appLocalizations.threeColumns, @@ -210,7 +209,7 @@ class Other { }; } - String getBackupFileName(){ + String getBackupFileName() { return "${appName}_backup_${DateTime.now().show}.zip"; } } diff --git a/lib/common/request.dart b/lib/common/request.dart index 857be5cc7..22a25f397 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -13,9 +13,6 @@ class Request { Request() { _dio = Dio(); - _dio.options = BaseOptions( - headers: {"User-Agent": globalState.appController.clashConfig.globalUa}, - ); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { @@ -52,6 +49,7 @@ class Request { .get( url, options: Options( + headers: {"User-Agent": globalState.appController.clashConfig.globalUa}, responseType: ResponseType.bytes, ), ) diff --git a/lib/common/string.dart b/lib/common/string.dart index 3dcc2d5f4..b83c89ecf 100644 --- a/lib/common/string.dart +++ b/lib/common/string.dart @@ -2,4 +2,10 @@ extension StringExtension on String { bool get isUrl { return RegExp(r'^(http|https|ftp)://').hasMatch(this); } + + int compareToLower(String other) { + return toLowerCase().compareTo( + other.toLowerCase(), + ); + } } diff --git a/lib/controller.dart b/lib/controller.dart index 5f86d4fcd..ac74e5d31 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -8,6 +8,7 @@ import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; +import 'package:lpinyin/lpinyin.dart'; import 'package:path/path.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -424,7 +425,10 @@ class AppController { List _sortOfName(List proxies) { return List.of(proxies) ..sort( - (a, b) => other.sortByChar(a.name, b.name), + (a, b) => other.sortByChar( + PinyinHelper.getPinyin(a.name), + PinyinHelper.getPinyin(b.name), + ), ); } diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index d5b9e4432..019e43322 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -52,6 +52,8 @@ enum TunStack { gvisor, system, mixed } enum AccessControlMode { acceptSelected, rejectSelected } +enum AccessSortType { none, name, time } + enum ProfileType { file, url } enum ResultType { success, error } diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 6d1b49330..e7d0c056d 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -1,4 +1,5 @@ -import 'package:collection/collection.dart'; +import 'dart:convert'; + import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/plugins/app.dart'; @@ -6,15 +7,9 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -extension AccessControlExtension on AccessControl { - List get currentList => switch (mode) { - AccessControlMode.acceptSelected => acceptList, - AccessControlMode.rejectSelected => rejectList, - }; -} - class AccessFragment extends StatefulWidget { const AccessFragment({super.key}); @@ -23,9 +18,13 @@ class AccessFragment extends StatefulWidget { } class _AccessFragmentState extends State { + List acceptList = []; + List rejectList = []; + @override void initState() { super.initState(); + _updateInitList(); WidgetsBinding.instance.addPostFrameCallback((_) { final appState = globalState.appController.appState; if (appState.packages.isEmpty) { @@ -36,297 +35,83 @@ class _AccessFragmentState extends State { }); } - Widget _buildAppProxyModePopup() { - final items = [ - CommonPopupMenuItem( - action: AccessControlMode.rejectSelected, - label: appLocalizations.blacklistMode, - ), - CommonPopupMenuItem( - action: AccessControlMode.acceptSelected, - label: appLocalizations.whitelistMode, - ), - ]; - return Selector( - selector: (_, config) => config.accessControl.mode, - builder: (context, mode, __) { - return CommonPopupMenu.radio( - icon: Icon( - Icons.mode_standby, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - items: items, - onSelected: (value) { - final config = context.read(); - config.accessControl = config.accessControl.copyWith( - mode: value, - ); - }, - selectedValue: mode, - ); - }, - ); + _updateInitList() { + final accessControl = globalState.appController.config.accessControl; + acceptList = accessControl.acceptList; + rejectList = accessControl.rejectList; } - Widget _buildFilterSystemAppButton() { - return Selector( - selector: (_, config) => config.accessControl.isFilterSystemApp, - builder: (context, isFilterSystemApp, __) { - final tooltip = isFilterSystemApp - ? appLocalizations.cancelFilterSystemApp - : appLocalizations.filterSystemApp; - return IconButton( - tooltip: tooltip, - onPressed: () { - final config = context.read(); - config.accessControl = config.accessControl.copyWith( - isFilterSystemApp: !isFilterSystemApp, - ); - }, - icon: isFilterSystemApp - ? const Icon(Icons.filter_list_off) - : const Icon(Icons.filter_list), - ); - }, - ); - } - - Widget _buildSearchButton(List packages) { + Widget _buildSearchButton() { return IconButton( tooltip: appLocalizations.search, onPressed: () { showSearch( context: context, delegate: AccessControlSearchDelegate( - packages: packages, + acceptList: acceptList, + rejectList: rejectList, ), - ).then((_) => {setState(() {})}); + ).then((_) => setState(() { + _updateInitList(); + })); }, icon: const Icon(Icons.search), ); } - // 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 Selector>( - selector: (_, appState) => appState.packages, - builder: (_, packages, ___) { - final accessControl = globalState.appController.config.accessControl; - final acceptList = accessControl.acceptList; - final rejectList = accessControl.rejectList; - final acceptPackages = packages.sorted((a, b) { - final isSelectA = acceptList.contains(a.packageName); - final isSelectB = acceptList.contains(b.packageName); - if (isSelectA && isSelectB) return 0; - if (isSelectA) return -1; - if (isSelectB) return 1; - return 0; - }); - final rejectPackages = packages.sorted((a, b) { - final isSelectA = rejectList.contains(a.packageName); - final isSelectB = rejectList.contains(b.packageName); - if (isSelectA && isSelectB) return 0; - if (isSelectA) return -1; - if (isSelectB) return 1; - return 0; - }); - return Selector( - selector: (_, config) => PackageListSelectorState( - accessControl: config.accessControl, - isAccessControl: config.isAccessControl, - ), - builder: (context, state, __) { - final accessControl = state.accessControl; - final isAccessControl = state.isAccessControl; - final isFilterSystemApp = accessControl.isFilterSystemApp; - final accessControlMode = accessControl.mode; - final packages = - accessControlMode == AccessControlMode.acceptSelected - ? acceptPackages - : rejectPackages; - final currentList = accessControl.currentList; - final currentPackages = isFilterSystemApp - ? packages - .where((element) => element.isSystem == false) - .toList() - : packages; - final packageNameList = - currentPackages.map((e) => e.packageName).toList(); - final valueList = currentList.intersection(packageNameList); - final describe = - accessControlMode == AccessControlMode.acceptSelected - ? appLocalizations.accessControlAllowDesc - : appLocalizations.accessControlNotAllowDesc; - return DisabledMask( - status: !isAccessControl, - 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, - ), - ), - Flexible( - child: Text( - "${valueList.length}", - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - ], - ), - ), - Flexible( - child: Text(describe), - ) - ], - ), - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - child: _buildSearchButton(currentPackages)), - Flexible(child: _buildFilterSystemAppButton()), - Flexible(child: _buildAppProxyModePopup()), - ], - ), - ], - ), - ), - ), - Expanded( - flex: 1, - 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, - ); - } - }, - ); - }, - ), - ), - ], + Widget _buildSelectedAllButton({ + required bool isSelectedAll, + required List allValueList, + }) { + final tooltip = isSelectedAll + ? appLocalizations.cancelSelectAll + : appLocalizations.selectAll; + return IconButton( + 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, + ), + }; + } + }, + icon: isSelectedAll + ? const Icon(Icons.deselect) + : const Icon(Icons.select_all), + ); + } + + Widget _buildSettingButton() { + return IconButton( + onPressed: () { + showSheet( + title: appLocalizations.proxiesSetting, + context: context, + builder: (_) { + return AccessControlWidget( + context: context, ); }, ); }, + icon: const Icon(Icons.tune), ); } @@ -363,7 +148,170 @@ class _AccessFragmentState extends State { ], ); }, - child: _buildPackageList(), + child: Selector>( + selector: (_, appState) => appState.packages, + builder: (_, packages, ___) { + return Selector2( + selector: (_, appState, config) => PackageListSelectorState( + accessControl: config.accessControl, + isAccessControl: config.isAccessControl, + packages: appState.packages, + ), + builder: (context, state, __) { + final accessControl = state.accessControl; + final isAccessControl = state.isAccessControl; + final accessControlMode = accessControl.mode; + final packages = state.getList( + accessControlMode == AccessControlMode.acceptSelected + ? acceptList + : rejectList, + ); + final currentList = accessControl.currentList; + final packageNameList = + packages.map((e) => e.packageName).toList(); + final valueList = currentList.intersection(packageNameList); + final describe = + accessControlMode == AccessControlMode.acceptSelected + ? appLocalizations.accessControlAllowDesc + : appLocalizations.accessControlNotAllowDesc; + return DisabledMask( + status: !isAccessControl, + 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, + ), + ), + Flexible( + child: Text( + "${valueList.length}", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + ], + ), + ), + Flexible( + child: Text(describe), + ) + ], + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: _buildSearchButton(), + ), + Flexible( + child: _buildSelectedAllButton( + isSelectedAll: valueList.length == + packageNameList.length, + allValueList: packageNameList, + ), + ), + Flexible( + child: _buildSettingButton(), + ), + ], + ), + ], + ), + ), + ), + Expanded( + flex: 1, + child: packages.isEmpty + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: packages.length, + itemBuilder: (_, index) { + final package = packages[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, + ); + } + }, + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ), ); } } @@ -430,23 +378,14 @@ class PackageListItem extends StatelessWidget { } class AccessControlSearchDelegate extends SearchDelegate { - final List packages; + List acceptList = []; + List rejectList = []; AccessControlSearchDelegate({ - required this.packages, + required this.acceptList, + required this.rejectList, }); - List get _results { - final lowQuery = query.toLowerCase(); - return packages - .where( - (package) => - package.label.toLowerCase().contains(lowQuery) || - package.packageName.contains(lowQuery), - ) - .toList(); - } - @override List? buildActions(BuildContext context) { return [ @@ -476,26 +415,39 @@ class AccessControlSearchDelegate extends SearchDelegate { ); } - Widget _packageList(List packages) { - return Selector( - selector: (_, config) => PackageListSelectorState( + Widget _packageList() { + final lowQuery = query.toLowerCase(); + return Selector2( + selector: (_, appState, config) => PackageListSelectorState( + packages: appState.packages, accessControl: config.accessControl, isAccessControl: config.isAccessControl, ), builder: (context, state, __) { final accessControl = state.accessControl; - final isAccessControl = state.isAccessControl; final accessControlMode = accessControl.mode; + final packages = state.getList( + accessControlMode == AccessControlMode.acceptSelected + ? acceptList + : rejectList, + ); + final queryPackages = packages + .where( + (package) => + package.label.toLowerCase().contains(lowQuery) || + package.packageName.contains(lowQuery), + ) + .toList(); + final isAccessControl = state.isAccessControl; final currentList = accessControl.currentList; - final packageNameList = - this.packages.map((e) => e.packageName).toList(); + final packageNameList = packages.map((e) => e.packageName).toList(); final valueList = currentList.intersection(packageNameList); return DisabledMask( status: !isAccessControl, child: ListView.builder( - itemCount: packages.length, + itemCount: queryPackages.length, itemBuilder: (_, index) { - final package = packages[index]; + final package = queryPackages[index]; return PackageListItem( key: Key(package.packageName), package: package, @@ -533,6 +485,268 @@ class AccessControlSearchDelegate extends SearchDelegate { @override Widget buildSuggestions(BuildContext context) { - return _packageList(_results); + return _packageList(); + } +} + +class AccessControlWidget extends StatelessWidget { + final BuildContext context; + + const AccessControlWidget({ + super.key, + required this.context, + }); + + IconData _getIconWithAccessControlMode(AccessControlMode mode) { + return switch (mode) { + AccessControlMode.acceptSelected => Icons.adjust_outlined, + AccessControlMode.rejectSelected => Icons.block_outlined, + }; + } + + String _getTextWithAccessControlMode(AccessControlMode mode) { + return switch (mode) { + AccessControlMode.acceptSelected => appLocalizations.whitelistMode, + AccessControlMode.rejectSelected => appLocalizations.blacklistMode, + }; + } + + String _getTextWithAccessSortType(AccessSortType type) { + return switch (type) { + AccessSortType.none => appLocalizations.defaultText, + AccessSortType.name => appLocalizations.name, + AccessSortType.time => appLocalizations.time, + }; + } + + IconData _getIconWithProxiesSortType(AccessSortType type) { + return switch (type) { + AccessSortType.none => Icons.sort, + AccessSortType.name => Icons.sort_by_alpha, + AccessSortType.time => Icons.timeline, + }; + } + + String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) { + return switch (isFilterSystemApp) { + true => appLocalizations.onlyOtherApps, + false => appLocalizations.allApps, + }; + } + + List _buildModeSetting() { + return generateSection( + title: appLocalizations.mode, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Selector( + selector: (_, config) => config.accessControl.mode, + builder: (_, accessControlMode, __) { + return Wrap( + spacing: 16, + children: [ + for (final item in AccessControlMode.values) + SettingInfoCard( + Info( + label: _getTextWithAccessControlMode(item), + iconData: _getIconWithAccessControlMode(item), + ), + isSelected: accessControlMode == item, + onPressed: () { + final config = globalState.appController.config; + config.accessControl = config.accessControl.copyWith( + mode: item, + ); + }, + ) + ], + ); + }, + ), + ) + ], + ); + } + + List _buildSortSetting() { + return generateSection( + title: appLocalizations.sort, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Selector( + selector: (_, config) => config.accessControl.sort, + builder: (_, accessSortType, __) { + return Wrap( + spacing: 16, + children: [ + for (final item in AccessSortType.values) + SettingInfoCard( + Info( + label: _getTextWithAccessSortType(item), + iconData: _getIconWithProxiesSortType(item), + ), + isSelected: accessSortType == item, + onPressed: () { + final config = globalState.appController.config; + config.accessControl = config.accessControl.copyWith( + sort: item, + ); + }, + ), + ], + ); + }, + ), + ), + ], + ); + } + + List _buildSourceSetting() { + return generateSection( + title: appLocalizations.source, + items: [ + SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + child: Selector( + selector: (_, config) => config.accessControl.isFilterSystemApp, + builder: (_, isFilterSystemApp, __) { + return Wrap( + spacing: 16, + children: [ + for (final item in [false, true]) + SettingTextCard( + _getTextWithIsFilterSystemApp(item), + isSelected: isFilterSystemApp == item, + onPressed: () { + final config = globalState.appController.config; + config.accessControl = config.accessControl.copyWith( + isFilterSystemApp: item, + ); + }, + ) + ], + ); + }, + ), + ) + ], + ); + } + + _intelligentSelected() async { + final appState = globalState.appController.appState; + final config = globalState.appController.config; + final accessControl = config.accessControl; + final packageNames = appState.packages + .where( + (item) => + accessControl.isFilterSystemApp ? item.isSystem == false : true, + ) + .map((item) => item.packageName); + Navigator.of(context).pop(); + final commonScaffoldState = context.commonScaffoldState; + if (commonScaffoldState?.mounted != true) return; + final selectedPackageNames = + (await commonScaffoldState?.loadingRun>( + () async { + return await app?.getChinaPackageNames() ?? []; + }, + )) + ?.toSet() ?? + {}; + final acceptList = packageNames + .where((item) => !selectedPackageNames.contains(item)) + .toList(); + final rejectList = packageNames + .where((item) => selectedPackageNames.contains(item)) + .toList(); + config.accessControl = accessControl.copyWith( + acceptList: acceptList, + rejectList: rejectList, + ); + } + + _copyToClipboard() async { + await globalState.safeRun(() { + final data = globalState.appController.config.accessControl.toJson(); + Clipboard.setData( + ClipboardData( + text: json.encode(data), + ), + ); + }); + if (!context.mounted) return; + Navigator.of(context).pop(); + } + + _pasteToClipboard() async { + await globalState.safeRun(() async { + final config = globalState.appController.config; + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text == null) return; + config.accessControl = AccessControl.fromJson( + json.decode(text), + ); + }); + if (!context.mounted) return; + Navigator.of(context).pop(); + } + + List _buildActionSetting() { + return generateSection( + title: appLocalizations.action, + items: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Wrap( + runSpacing: 16, + spacing: 16, + children: [ + CommonChip( + avatar: const Icon(Icons.auto_awesome), + label: appLocalizations.intelligentSelected, + onPressed: _intelligentSelected, + ), + CommonChip( + avatar: const Icon(Icons.paste), + label: appLocalizations.clipboardImport, + onPressed: _pasteToClipboard, + ), + CommonChip( + avatar: const Icon(Icons.content_copy), + label: appLocalizations.clipboardExport, + onPressed: _copyToClipboard, + ) + ], + ), + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildModeSetting(), + ..._buildSortSetting(), + ..._buildSourceSetting(), + ..._buildActionSetting(), + ], + ), + ); } } diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index cb6c2fef0..722d07181 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:typed_data'; import 'package:fl_clash/common/common.dart'; @@ -7,7 +6,6 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/dav.dart'; import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/text.dart'; @@ -75,10 +73,11 @@ class BackupAndRecovery extends StatelessWidget { final res = await commonScaffoldState?.loadingRun( () async { final backupData = await globalState.appController.backupData(); - await picker.saveFile( + final value = await picker.saveFile( other.getBackupFileName(), Uint8List.fromList(backupData), ); + if(value == null) return false; return true; }, title: appLocalizations.backup, diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index b656045c0..d01a48dcc 100644 --- a/lib/fragments/dashboard/network_detection.dart +++ b/lib/fragments/dashboard/network_detection.dart @@ -1,5 +1,4 @@ import 'package:country_flags/country_flags.dart'; -import 'package:dio/dio.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 0a53acecc..a379093f5 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -11,8 +11,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'view_profile.dart'; - class EditProfile extends StatefulWidget { final Profile profile; final BuildContext context; diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 6113219ae..f7e1aa8e4 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -77,9 +77,9 @@ class _ProfilesFragmentState extends State { _initScaffoldState() { WidgetsBinding.instance.addPostFrameCallback( (_) { + if (!context.mounted) return; final commonScaffoldState = context.findAncestorStateOfType(); - if (!context.mounted) return; commonScaffoldState?.actions = [ IconButton( onPressed: () { diff --git a/lib/fragments/proxies/common.dart b/lib/fragments/proxies/common.dart index 46dfca682..a78d9d10d 100644 --- a/lib/fragments/proxies/common.dart +++ b/lib/fragments/proxies/common.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:fl_clash/clash/clash.dart'; -import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart index dd439cf2b..d78c75bc5 100644 --- a/lib/fragments/proxies/providers.dart +++ b/lib/fragments/proxies/providers.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/app.dart'; @@ -50,7 +53,6 @@ class _ProvidersState extends State { ); await clashCore.updateExternalProvider( providerName: provider.name, - providerType: provider.type, ); appState.setProvider( clashCore.getExternalProvider(provider.name), @@ -58,6 +60,7 @@ class _ProvidersState extends State { }, ); await Future.wait(updateProviders); + await globalState.appController.updateGroupDebounce(); } @override @@ -91,28 +94,48 @@ class ProviderItem extends StatelessWidget { required this.provider, }); - _handleUpdateProfile() async { - await globalState.safeRun(updateProvider); + _handleUpdateProvider() async { + await globalState.safeRun(() async { + final appState = globalState.appController.appState; + if (provider.vehicleType != "HTTP") return; + await globalState.safeRun(() async { + appState.setProvider( + provider.copyWith( + isUpdating: true, + ), + ); + final message = await clashCore.updateExternalProvider( + providerName: provider.name, + ); + if (message.isNotEmpty) throw message; + }); + appState.setProvider( + clashCore.getExternalProvider(provider.name), + ); + }); + await globalState.appController.updateGroupDebounce(); } - updateProvider() async { - final appState = globalState.appController.appState; - if (provider.vehicleType != "HTTP") return; - await globalState.safeRun(() async { - appState.setProvider( - provider.copyWith( - isUpdating: true, - ), + _handleSideLoadProvider() async { + await globalState.safeRun(() async { + final platformFile = await picker.pickerFile(); + final appState = globalState.appController.appState; + final bytes = platformFile?.bytes; + if (bytes == null) return; + final file = await File(provider.path).create(recursive: true); + await file.writeAsBytes(bytes); + final providerName = provider.name; + var message = await clashCore.sideLoadExternalProvider( + providerName: providerName, + data: utf8.decode(bytes), ); - final message = await clashCore.updateExternalProvider( - providerName: provider.name, - providerType: provider.type, + if (message.isNotEmpty) throw message; + appState.setProvider( + clashCore.getExternalProvider(provider.name), ); if (message.isNotEmpty) throw message; }); - appState.setProvider( - clashCore.getExternalProvider(provider.name), - ); + await globalState.appController.updateGroupDebounce(); } String _buildProviderDesc() { @@ -153,18 +176,16 @@ class ProviderItem extends StatelessWidget { runSpacing: 6, spacing: 12, children: [ - // CommonChip( - // avatar: const Icon(Icons.upload), - // label: appLocalizations.upload, - // onPressed: () {}, - // ), + CommonChip( + avatar: const Icon(Icons.upload), + label: appLocalizations.upload, + onPressed: _handleSideLoadProvider, + ), if (provider.vehicleType == "HTTP") CommonChip( avatar: const Icon(Icons.sync), label: appLocalizations.sync, - onPressed: () { - _handleUpdateProfile(); - }, + onPressed: _handleUpdateProvider, ), ], ), diff --git a/lib/fragments/proxies/setting.dart b/lib/fragments/proxies/setting.dart index c884455eb..3d0a6e0d1 100644 --- a/lib/fragments/proxies/setting.dart +++ b/lib/fragments/proxies/setting.dart @@ -189,74 +189,4 @@ class ProxiesSettingWidget extends StatelessWidget { ), ); } -} - -class SettingInfoCard extends StatelessWidget { - final Info info; - final bool? isSelected; - final VoidCallback onPressed; - - const SettingInfoCard( - this.info, { - super.key, - this.isSelected, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return CommonCard( - isSelected: isSelected, - onPressed: onPressed, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Flexible( - child: Icon(info.iconData), - ), - const SizedBox( - width: 8, - ), - Flexible( - child: Text( - info.label, - style: context.textTheme.bodyMedium, - ), - ), - ], - ), - ), - ); - } -} - -class SettingTextCard extends StatelessWidget { - final String text; - final bool? isSelected; - final VoidCallback onPressed; - - const SettingTextCard( - this.text, { - super.key, - this.isSelected, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return CommonCard( - onPressed: onPressed, - isSelected: isSelected, - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - text, - style: context.textTheme.bodyMedium, - ), - ), - ); - } -} +} \ No newline at end of file diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index d8a26b64c..1736b1b0d 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -1,7 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/fragments/proxies/setting.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index d6a651f03..86a9e99de 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -182,9 +182,9 @@ class _GeoDataListItemState extends State { updateGeoDateItem() async { isUpdating.value = true; try { - final message = await clashCore.updateExternalProvider( - providerName: geoItem.fileName, - providerType: geoItem.label, + final message = await clashCore.updateGeoData( + geoName: geoItem.fileName, + geoType: geoItem.label, ); if (message.isNotEmpty) throw message; } catch (e) { diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 43156a6d1..abdcfc73b 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -227,5 +227,14 @@ "remoteBackupDesc": "Backup local data to WebDAV", "remoteRecoveryDesc": "Recovery data from WebDAV", "localBackupDesc": "Backup local data to local", - "localRecoveryDesc": "Recovery data from file" + "localRecoveryDesc": "Recovery data from file", + "mode": "Mode", + "time": "Time", + "source": "Source", + "allApps": "All apps", + "onlyOtherApps": "Only third-party apps", + "action": "Action", + "intelligentSelected": "Intelligent selection", + "clipboardImport": "Clipboard import", + "clipboardExport": "Export clipboard" } \ 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 7a925b623..b106628cf 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -227,5 +227,14 @@ "remoteBackupDesc": "备份数据到WebDAV", "remoteRecoveryDesc": "通过WebDAV恢复数据", "localBackupDesc": "备份数据到本地", - "localRecoveryDesc": "通过文件恢复数据" + "localRecoveryDesc": "通过文件恢复数据", + "mode": "模式", + "time": "时间", + "source": "来源", + "allApps": "所有应用", + "onlyOtherApps": "仅第三方应用", + "action": "操作", + "intelligentSelected": "智能选择", + "clipboardImport": "剪贴板导入", + "clipboardExport": "导出剪贴板" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 885c3951b..457f4cd59 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -33,6 +33,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Account"), "accountTip": MessageLookupByLibrary.simpleMessage("Account cannot be empty"), + "action": MessageLookupByLibrary.simpleMessage("Action"), "add": MessageLookupByLibrary.simpleMessage("Add"), "address": MessageLookupByLibrary.simpleMessage("Address"), "addressHelp": @@ -40,6 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { "addressTip": MessageLookupByLibrary.simpleMessage( "Please enter a valid WebDAV address"), "ago": MessageLookupByLibrary.simpleMessage(" Ago"), + "allApps": MessageLookupByLibrary.simpleMessage("All apps"), "allowBypass": MessageLookupByLibrary.simpleMessage( "Allow applications to bypass VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage( @@ -89,6 +91,10 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdateError": MessageLookupByLibrary.simpleMessage( "The current application is already the latest version"), "checking": MessageLookupByLibrary.simpleMessage("Checking..."), + "clipboardExport": + MessageLookupByLibrary.simpleMessage("Export clipboard"), + "clipboardImport": + MessageLookupByLibrary.simpleMessage("Clipboard import"), "columns": MessageLookupByLibrary.simpleMessage("Columns"), "compatible": MessageLookupByLibrary.simpleMessage("Compatibility mode"), @@ -167,6 +173,8 @@ class MessageLookup extends MessageLookupByLibrary { "infiniteTime": MessageLookupByLibrary.simpleMessage("Long term effective"), "init": MessageLookupByLibrary.simpleMessage("Init"), + "intelligentSelected": + MessageLookupByLibrary.simpleMessage("Intelligent selection"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( "When turned on it will be able to receive IPv6 traffic"), @@ -193,6 +201,7 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage( "Modify the default system exit event"), "minutes": MessageLookupByLibrary.simpleMessage("Minutes"), + "mode": MessageLookupByLibrary.simpleMessage("Mode"), "months": MessageLookupByLibrary.simpleMessage("Months"), "more": MessageLookupByLibrary.simpleMessage("More"), "name": MessageLookupByLibrary.simpleMessage("Name"), @@ -216,6 +225,8 @@ class MessageLookup extends MessageLookupByLibrary { "No profile, Please add a profile"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"), "oneColumn": MessageLookupByLibrary.simpleMessage("One column"), + "onlyOtherApps": + MessageLookupByLibrary.simpleMessage("Only third-party apps"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("Only statistics proxy"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( @@ -300,6 +311,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Start in the background"), "size": MessageLookupByLibrary.simpleMessage("Size"), "sort": MessageLookupByLibrary.simpleMessage("Sort"), + "source": MessageLookupByLibrary.simpleMessage("Source"), "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "style": MessageLookupByLibrary.simpleMessage("Style"), @@ -322,6 +334,7 @@ class MessageLookup extends MessageLookupByLibrary { "Set dark mode,adjust the color"), "themeMode": MessageLookupByLibrary.simpleMessage("Theme mode"), "threeColumns": MessageLookupByLibrary.simpleMessage("Three columns"), + "time": MessageLookupByLibrary.simpleMessage("Time"), "tip": MessageLookupByLibrary.simpleMessage("tip"), "tools": MessageLookupByLibrary.simpleMessage("Tools"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 99e2e58fe..78e0f6798 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -31,11 +31,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), "account": MessageLookupByLibrary.simpleMessage("账号"), "accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"), + "action": MessageLookupByLibrary.simpleMessage("操作"), "add": MessageLookupByLibrary.simpleMessage("添加"), "address": MessageLookupByLibrary.simpleMessage("地址"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "ago": MessageLookupByLibrary.simpleMessage("前"), + "allApps": MessageLookupByLibrary.simpleMessage("所有应用"), "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), @@ -73,6 +75,8 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checking": MessageLookupByLibrary.simpleMessage("检测中..."), + "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), + "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "columns": MessageLookupByLibrary.simpleMessage("列数"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatibleDesc": @@ -136,6 +140,7 @@ class MessageLookup extends MessageLookupByLibrary { "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "init": MessageLookupByLibrary.simpleMessage("初始化"), + "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), @@ -157,6 +162,7 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), "minutes": MessageLookupByLibrary.simpleMessage("分钟"), + "mode": MessageLookupByLibrary.simpleMessage("模式"), "months": MessageLookupByLibrary.simpleMessage("月"), "more": MessageLookupByLibrary.simpleMessage("更多"), "name": MessageLookupByLibrary.simpleMessage("名称"), @@ -176,6 +182,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "oneColumn": MessageLookupByLibrary.simpleMessage("一列"), + "onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"), @@ -241,6 +248,7 @@ class MessageLookup extends MessageLookupByLibrary { "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), "size": MessageLookupByLibrary.simpleMessage("尺寸"), "sort": MessageLookupByLibrary.simpleMessage("排序"), + "source": MessageLookupByLibrary.simpleMessage("来源"), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "style": MessageLookupByLibrary.simpleMessage("风格"), @@ -261,6 +269,7 @@ class MessageLookup extends MessageLookupByLibrary { "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"), + "time": MessageLookupByLibrary.simpleMessage("时间"), "tip": MessageLookupByLibrary.simpleMessage("提示"), "tools": MessageLookupByLibrary.simpleMessage("工具"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 796b6fac3..66ba8604d 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2339,6 +2339,96 @@ class AppLocalizations { args: [], ); } + + /// `Mode` + String get mode { + return Intl.message( + 'Mode', + name: 'mode', + desc: '', + args: [], + ); + } + + /// `Time` + String get time { + return Intl.message( + 'Time', + name: 'time', + desc: '', + args: [], + ); + } + + /// `Source` + String get source { + return Intl.message( + 'Source', + name: 'source', + desc: '', + args: [], + ); + } + + /// `All apps` + String get allApps { + return Intl.message( + 'All apps', + name: 'allApps', + desc: '', + args: [], + ); + } + + /// `Only third-party apps` + String get onlyOtherApps { + return Intl.message( + 'Only third-party apps', + name: 'onlyOtherApps', + desc: '', + args: [], + ); + } + + /// `Action` + String get action { + return Intl.message( + 'Action', + name: 'action', + desc: '', + args: [], + ); + } + + /// `Intelligent selection` + String get intelligentSelected { + return Intl.message( + 'Intelligent selection', + name: 'intelligentSelected', + desc: '', + args: [], + ); + } + + /// `Clipboard import` + String get clipboardImport { + return Intl.message( + 'Clipboard import', + name: 'clipboardImport', + desc: '', + args: [], + ); + } + + /// `Export clipboard` + String get clipboardExport { + return Intl.message( + 'Export clipboard', + name: 'clipboardExport', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index ef0dbcb10..5879be1bf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -130,13 +130,13 @@ class ServiceMessageHandler with ServiceMessageListener { final Function(Fd fd) _onProtect; final Function(Process process) _onProcess; final Function(String runTime) _onStarted; - final Function(String groupName) _onLoaded; + final Function(String providerName) _onLoaded; const ServiceMessageHandler({ required Function(Fd fd) onProtect, required Function(Process process) onProcess, required Function(String runTime) onStarted, - required Function(String groupName) onLoaded, + required Function(String providerName) onLoaded, }) : _onProtect = onProtect, _onProcess = onProcess, _onStarted = onStarted, @@ -158,8 +158,8 @@ class ServiceMessageHandler with ServiceMessageListener { } @override - onLoaded(String groupName) { - _onLoaded(groupName); + onLoaded(String providerName) { + _onLoaded(providerName); } } diff --git a/lib/models/app.dart b/lib/models/app.dart index 071ebd3e7..a26da6724 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -353,7 +353,8 @@ class AppState with ChangeNotifier { } } - setProvider(ExternalProvider provider) { + setProvider(ExternalProvider? provider) { + if(provider == null) return; final index = _providers.indexWhere((item) => item.name == provider.name); if (index == -1) return; _providers = List.from(_providers)..[index] = provider; diff --git a/lib/models/config.dart b/lib/models/config.dart index 11147fe5e..ea1da7238 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -18,6 +18,7 @@ class AccessControl with _$AccessControl { @Default(AccessControlMode.rejectSelected) AccessControlMode mode, @Default([]) List acceptList, @Default([]) List rejectList, + @Default(AccessSortType.none) AccessSortType sort, @Default(true) bool isFilterSystemApp, }) = _AccessControl; @@ -25,6 +26,13 @@ class AccessControl with _$AccessControl { _$AccessControlFromJson(json); } +extension AccessControlExt on AccessControl { + List get currentList => switch (mode) { + AccessControlMode.acceptSelected => acceptList, + AccessControlMode.rejectSelected => rejectList, + }; +} + @freezed class CoreState with _$CoreState { const factory CoreState({ diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 18e0567ac..8eb42c9fb 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -23,6 +23,7 @@ mixin _$AccessControl { AccessControlMode get mode => throw _privateConstructorUsedError; List get acceptList => throw _privateConstructorUsedError; List get rejectList => throw _privateConstructorUsedError; + AccessSortType get sort => throw _privateConstructorUsedError; bool get isFilterSystemApp => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -41,6 +42,7 @@ abstract class $AccessControlCopyWith<$Res> { {AccessControlMode mode, List acceptList, List rejectList, + AccessSortType sort, bool isFilterSystemApp}); } @@ -60,6 +62,7 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl> Object? mode = null, Object? acceptList = null, Object? rejectList = null, + Object? sort = null, Object? isFilterSystemApp = null, }) { return _then(_value.copyWith( @@ -75,6 +78,10 @@ class _$AccessControlCopyWithImpl<$Res, $Val extends AccessControl> ? _value.rejectList : rejectList // ignore: cast_nullable_to_non_nullable as List, + sort: null == sort + ? _value.sort + : sort // ignore: cast_nullable_to_non_nullable + as AccessSortType, isFilterSystemApp: null == isFilterSystemApp ? _value.isFilterSystemApp : isFilterSystemApp // ignore: cast_nullable_to_non_nullable @@ -95,6 +102,7 @@ abstract class _$$AccessControlImplCopyWith<$Res> {AccessControlMode mode, List acceptList, List rejectList, + AccessSortType sort, bool isFilterSystemApp}); } @@ -112,6 +120,7 @@ class __$$AccessControlImplCopyWithImpl<$Res> Object? mode = null, Object? acceptList = null, Object? rejectList = null, + Object? sort = null, Object? isFilterSystemApp = null, }) { return _then(_$AccessControlImpl( @@ -127,6 +136,10 @@ class __$$AccessControlImplCopyWithImpl<$Res> ? _value._rejectList : rejectList // ignore: cast_nullable_to_non_nullable as List, + sort: null == sort + ? _value.sort + : sort // ignore: cast_nullable_to_non_nullable + as AccessSortType, isFilterSystemApp: null == isFilterSystemApp ? _value.isFilterSystemApp : isFilterSystemApp // ignore: cast_nullable_to_non_nullable @@ -142,6 +155,7 @@ class _$AccessControlImpl implements _AccessControl { {this.mode = AccessControlMode.rejectSelected, final List acceptList = const [], final List rejectList = const [], + this.sort = AccessSortType.none, this.isFilterSystemApp = true}) : _acceptList = acceptList, _rejectList = rejectList; @@ -170,13 +184,16 @@ class _$AccessControlImpl implements _AccessControl { return EqualUnmodifiableListView(_rejectList); } + @override + @JsonKey() + final AccessSortType sort; @override @JsonKey() final bool isFilterSystemApp; @override String toString() { - return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, isFilterSystemApp: $isFilterSystemApp)'; + return 'AccessControl(mode: $mode, acceptList: $acceptList, rejectList: $rejectList, sort: $sort, isFilterSystemApp: $isFilterSystemApp)'; } @override @@ -189,6 +206,7 @@ class _$AccessControlImpl implements _AccessControl { .equals(other._acceptList, _acceptList) && const DeepCollectionEquality() .equals(other._rejectList, _rejectList) && + (identical(other.sort, sort) || other.sort == sort) && (identical(other.isFilterSystemApp, isFilterSystemApp) || other.isFilterSystemApp == isFilterSystemApp)); } @@ -200,6 +218,7 @@ class _$AccessControlImpl implements _AccessControl { mode, const DeepCollectionEquality().hash(_acceptList), const DeepCollectionEquality().hash(_rejectList), + sort, isFilterSystemApp); @JsonKey(ignore: true) @@ -221,6 +240,7 @@ abstract class _AccessControl implements AccessControl { {final AccessControlMode mode, final List acceptList, final List rejectList, + final AccessSortType sort, final bool isFilterSystemApp}) = _$AccessControlImpl; factory _AccessControl.fromJson(Map json) = @@ -233,6 +253,8 @@ abstract class _AccessControl implements AccessControl { @override List get rejectList; @override + AccessSortType get sort; + @override bool get isFilterSystemApp; @override @JsonKey(ignore: true) diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index dff1c142a..de7626b9a 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -117,6 +117,8 @@ _$AccessControlImpl _$$AccessControlImplFromJson(Map json) => ?.map((e) => e as String) .toList() ?? const [], + sort: $enumDecodeNullable(_$AccessSortTypeEnumMap, json['sort']) ?? + AccessSortType.none, isFilterSystemApp: json['isFilterSystemApp'] as bool? ?? true, ); @@ -125,6 +127,7 @@ Map _$$AccessControlImplToJson(_$AccessControlImpl instance) => 'mode': _$AccessControlModeEnumMap[instance.mode]!, 'acceptList': instance.acceptList, 'rejectList': instance.rejectList, + 'sort': _$AccessSortTypeEnumMap[instance.sort]!, 'isFilterSystemApp': instance.isFilterSystemApp, }; @@ -133,6 +136,12 @@ const _$AccessControlModeEnumMap = { AccessControlMode.rejectSelected: 'rejectSelected', }; +const _$AccessSortTypeEnumMap = { + AccessSortType.none: 'none', + AccessSortType.name: 'name', + AccessSortType.time: 'time', +}; + _$CoreStateImpl _$$CoreStateImplFromJson(Map json) => _$CoreStateImpl( accessControl: json['accessControl'] == null diff --git a/lib/models/generated/package.freezed.dart b/lib/models/generated/package.freezed.dart index 8109b08e1..5b479a570 100644 --- a/lib/models/generated/package.freezed.dart +++ b/lib/models/generated/package.freezed.dart @@ -23,6 +23,7 @@ mixin _$Package { String get packageName => throw _privateConstructorUsedError; String get label => throw _privateConstructorUsedError; bool get isSystem => throw _privateConstructorUsedError; + int get firstInstallTime => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -34,7 +35,8 @@ abstract class $PackageCopyWith<$Res> { factory $PackageCopyWith(Package value, $Res Function(Package) then) = _$PackageCopyWithImpl<$Res, Package>; @useResult - $Res call({String packageName, String label, bool isSystem}); + $Res call( + {String packageName, String label, bool isSystem, int firstInstallTime}); } /// @nodoc @@ -53,6 +55,7 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package> Object? packageName = null, Object? label = null, Object? isSystem = null, + Object? firstInstallTime = null, }) { return _then(_value.copyWith( packageName: null == packageName @@ -67,6 +70,10 @@ class _$PackageCopyWithImpl<$Res, $Val extends Package> ? _value.isSystem : isSystem // ignore: cast_nullable_to_non_nullable as bool, + firstInstallTime: null == firstInstallTime + ? _value.firstInstallTime + : firstInstallTime // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } } @@ -78,7 +85,8 @@ abstract class _$$PackageImplCopyWith<$Res> implements $PackageCopyWith<$Res> { __$$PackageImplCopyWithImpl<$Res>; @override @useResult - $Res call({String packageName, String label, bool isSystem}); + $Res call( + {String packageName, String label, bool isSystem, int firstInstallTime}); } /// @nodoc @@ -95,6 +103,7 @@ class __$$PackageImplCopyWithImpl<$Res> Object? packageName = null, Object? label = null, Object? isSystem = null, + Object? firstInstallTime = null, }) { return _then(_$PackageImpl( packageName: null == packageName @@ -109,6 +118,10 @@ class __$$PackageImplCopyWithImpl<$Res> ? _value.isSystem : isSystem // ignore: cast_nullable_to_non_nullable as bool, + firstInstallTime: null == firstInstallTime + ? _value.firstInstallTime + : firstInstallTime // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -117,7 +130,10 @@ class __$$PackageImplCopyWithImpl<$Res> @JsonSerializable() class _$PackageImpl implements _Package { const _$PackageImpl( - {required this.packageName, required this.label, required this.isSystem}); + {required this.packageName, + required this.label, + required this.isSystem, + required this.firstInstallTime}); factory _$PackageImpl.fromJson(Map json) => _$$PackageImplFromJson(json); @@ -128,10 +144,12 @@ class _$PackageImpl implements _Package { final String label; @override final bool isSystem; + @override + final int firstInstallTime; @override String toString() { - return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem)'; + return 'Package(packageName: $packageName, label: $label, isSystem: $isSystem, firstInstallTime: $firstInstallTime)'; } @override @@ -143,12 +161,15 @@ class _$PackageImpl implements _Package { other.packageName == packageName) && (identical(other.label, label) || other.label == label) && (identical(other.isSystem, isSystem) || - other.isSystem == isSystem)); + other.isSystem == isSystem) && + (identical(other.firstInstallTime, firstInstallTime) || + other.firstInstallTime == firstInstallTime)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, packageName, label, isSystem); + int get hashCode => + Object.hash(runtimeType, packageName, label, isSystem, firstInstallTime); @JsonKey(ignore: true) @override @@ -168,7 +189,8 @@ abstract class _Package implements Package { const factory _Package( {required final String packageName, required final String label, - required final bool isSystem}) = _$PackageImpl; + required final bool isSystem, + required final int firstInstallTime}) = _$PackageImpl; factory _Package.fromJson(Map json) = _$PackageImpl.fromJson; @@ -179,6 +201,8 @@ abstract class _Package implements Package { @override bool get isSystem; @override + int get firstInstallTime; + @override @JsonKey(ignore: true) _$$PackageImplCopyWith<_$PackageImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/generated/package.g.dart b/lib/models/generated/package.g.dart index 97b539c1a..3d212e7fc 100644 --- a/lib/models/generated/package.g.dart +++ b/lib/models/generated/package.g.dart @@ -11,6 +11,7 @@ _$PackageImpl _$$PackageImplFromJson(Map json) => packageName: json['packageName'] as String, label: json['label'] as String, isSystem: json['isSystem'] as bool, + firstInstallTime: (json['firstInstallTime'] as num).toInt(), ); Map _$$PackageImplToJson(_$PackageImpl instance) => @@ -18,4 +19,5 @@ Map _$$PackageImplToJson(_$PackageImpl instance) => 'packageName': instance.packageName, 'label': instance.label, 'isSystem': instance.isSystem, + 'firstInstallTime': instance.firstInstallTime, }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 1405f1dae..ff2d62ac8 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -2372,6 +2372,7 @@ abstract class _MoreToolsSelectorState implements MoreToolsSelectorState { /// @nodoc mixin _$PackageListSelectorState { + List get packages => throw _privateConstructorUsedError; AccessControl get accessControl => throw _privateConstructorUsedError; bool get isAccessControl => throw _privateConstructorUsedError; @@ -2386,7 +2387,10 @@ abstract class $PackageListSelectorStateCopyWith<$Res> { $Res Function(PackageListSelectorState) then) = _$PackageListSelectorStateCopyWithImpl<$Res, PackageListSelectorState>; @useResult - $Res call({AccessControl accessControl, bool isAccessControl}); + $Res call( + {List packages, + AccessControl accessControl, + bool isAccessControl}); $AccessControlCopyWith<$Res> get accessControl; } @@ -2405,10 +2409,15 @@ class _$PackageListSelectorStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ + Object? packages = null, Object? accessControl = null, Object? isAccessControl = null, }) { return _then(_value.copyWith( + packages: null == packages + ? _value.packages + : packages // ignore: cast_nullable_to_non_nullable + as List, accessControl: null == accessControl ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable @@ -2438,7 +2447,10 @@ abstract class _$$PackageListSelectorStateImplCopyWith<$Res> __$$PackageListSelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({AccessControl accessControl, bool isAccessControl}); + $Res call( + {List packages, + AccessControl accessControl, + bool isAccessControl}); @override $AccessControlCopyWith<$Res> get accessControl; @@ -2457,10 +2469,15 @@ class __$$PackageListSelectorStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? packages = null, Object? accessControl = null, Object? isAccessControl = null, }) { return _then(_$PackageListSelectorStateImpl( + packages: null == packages + ? _value._packages + : packages // ignore: cast_nullable_to_non_nullable + as List, accessControl: null == accessControl ? _value.accessControl : accessControl // ignore: cast_nullable_to_non_nullable @@ -2477,7 +2494,18 @@ class __$$PackageListSelectorStateImplCopyWithImpl<$Res> class _$PackageListSelectorStateImpl implements _PackageListSelectorState { const _$PackageListSelectorStateImpl( - {required this.accessControl, required this.isAccessControl}); + {required final List packages, + required this.accessControl, + required this.isAccessControl}) + : _packages = packages; + + final List _packages; + @override + List get packages { + if (_packages is EqualUnmodifiableListView) return _packages; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_packages); + } @override final AccessControl accessControl; @@ -2486,7 +2514,7 @@ class _$PackageListSelectorStateImpl implements _PackageListSelectorState { @override String toString() { - return 'PackageListSelectorState(accessControl: $accessControl, isAccessControl: $isAccessControl)'; + return 'PackageListSelectorState(packages: $packages, accessControl: $accessControl, isAccessControl: $isAccessControl)'; } @override @@ -2494,6 +2522,7 @@ class _$PackageListSelectorStateImpl implements _PackageListSelectorState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PackageListSelectorStateImpl && + const DeepCollectionEquality().equals(other._packages, _packages) && (identical(other.accessControl, accessControl) || other.accessControl == accessControl) && (identical(other.isAccessControl, isAccessControl) || @@ -2501,7 +2530,11 @@ class _$PackageListSelectorStateImpl implements _PackageListSelectorState { } @override - int get hashCode => Object.hash(runtimeType, accessControl, isAccessControl); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_packages), + accessControl, + isAccessControl); @JsonKey(ignore: true) @override @@ -2513,9 +2546,12 @@ class _$PackageListSelectorStateImpl implements _PackageListSelectorState { abstract class _PackageListSelectorState implements PackageListSelectorState { const factory _PackageListSelectorState( - {required final AccessControl accessControl, + {required final List packages, + required final AccessControl accessControl, required final bool isAccessControl}) = _$PackageListSelectorStateImpl; + @override + List get packages; @override AccessControl get accessControl; @override diff --git a/lib/models/package.dart b/lib/models/package.dart index abf406ff1..31eb642e8 100644 --- a/lib/models/package.dart +++ b/lib/models/package.dart @@ -9,6 +9,7 @@ class Package with _$Package { required String packageName, required String label, required bool isSystem, + required int firstInstallTime, }) = _Package; factory Package.fromJson(Map json) => diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 0d18aaba0..06cf90ca2 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -1,7 +1,10 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lpinyin/lpinyin.dart'; part 'generated/selector.freezed.dart'; @@ -132,11 +135,43 @@ class MoreToolsSelectorState with _$MoreToolsSelectorState { @freezed class PackageListSelectorState with _$PackageListSelectorState { const factory PackageListSelectorState({ + required List packages, required AccessControl accessControl, required bool isAccessControl, }) = _PackageListSelectorState; } +extension PackageListSelectorStateExt on PackageListSelectorState { + List getList(List selectedList) { + final isFilterSystemApp = accessControl.isFilterSystemApp; + final sort = accessControl.sort; + return packages + .where((item) => isFilterSystemApp ? item.isSystem == false : true) + .sorted( + (a, b) { + return switch (sort) { + AccessSortType.none => 0, + AccessSortType.name => + other.sortByChar( + PinyinHelper.getPinyin(a.label), + PinyinHelper.getPinyin(b.label), + ), + AccessSortType.time => a.firstInstallTime.compareTo(b.firstInstallTime), + }; + }, + ).sorted( + (a, b) { + final isSelectA = selectedList.contains(a.packageName); + final isSelectB = selectedList.contains(b.packageName); + if (isSelectA && isSelectB) return 0; + if (isSelectA) return -1; + if (isSelectB) return 1; + return 0; + }, + ); + } +} + @freezed class ColumnsSelectorState with _$ColumnsSelectorState { const factory ColumnsSelectorState({ @@ -153,11 +188,10 @@ class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState { }) = _ProxiesListHeaderSelectorState; } - @freezed class ProxiesActionsState with _$ProxiesActionsState { const factory ProxiesActionsState({ required bool isCurrent, required bool hasProvider, }) = _ProxiesActionsState; -} \ No newline at end of file +} diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index d55cdfc5e..df1ef6059 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -48,6 +48,16 @@ class App { }); } + Future> getChinaPackageNames() async { + final packageNamesString = + await methodChannel.invokeMethod("getChinaPackageNames"); + return Isolate.run>(() { + final List packageNamesRaw = + packageNamesString != null ? json.decode(packageNamesString) : []; + return packageNamesRaw.map((e) => e.toString()).toList(); + }); + } + Future openFile(String path) async { return await methodChannel.invokeMethod("openFile", { "path": path, diff --git a/lib/plugins/proxy.dart b/lib/plugins/proxy.dart index ca212ad22..88d0cff97 100644 --- a/lib/plugins/proxy.dart +++ b/lib/plugins/proxy.dart @@ -81,7 +81,6 @@ class Proxy extends ProxyPlatform { bool get isStart => startTime != null && startTime!.isBeforeNow; onStarted(int? fd) { - debugPrint("onStarted ==> $fd"); if (fd == null) return; if (receiver != null) { receiver!.close(); diff --git a/lib/widgets/clash_container.dart b/lib/widgets/clash_container.dart index e38736b91..96897d601 100644 --- a/lib/widgets/clash_container.dart +++ b/lib/widgets/clash_container.dart @@ -79,10 +79,11 @@ class _ClashContainerState extends State } @override - void onDelay(Delay delay) { + Future onDelay(Delay delay) async { final appController = globalState.appController; appController.setDelay(delay); super.onDelay(delay); + await globalState.appController.updateGroupDebounce(); } @override diff --git a/lib/widgets/setting.dart b/lib/widgets/setting.dart new file mode 100644 index 000000000..111252cc3 --- /dev/null +++ b/lib/widgets/setting.dart @@ -0,0 +1,74 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; + +import 'card.dart'; + +class SettingInfoCard extends StatelessWidget { + final Info info; + final bool? isSelected; + final VoidCallback onPressed; + + const SettingInfoCard( + this.info, { + super.key, + this.isSelected, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return CommonCard( + isSelected: isSelected, + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + child: Icon(info.iconData), + ), + const SizedBox( + width: 8, + ), + Flexible( + child: Text( + info.label, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ), + ); + } +} + +class SettingTextCard extends StatelessWidget { + final String text; + final bool? isSelected; + final VoidCallback onPressed; + + const SettingTextCard( + this.text, { + super.key, + this.isSelected, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return CommonCard( + onPressed: onPressed, + isSelected: isSelected, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + text, + style: context.textTheme.bodyMedium, + ), + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index e1ab8a9aa..2aabbcf0f 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -24,4 +24,5 @@ export 'fade_box.dart'; export 'app_state_container.dart'; export 'text.dart'; export 'connection_item.dart'; -export 'builder.dart'; \ No newline at end of file +export 'builder.dart'; +export 'setting.dart'; \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 377510822..e7c8b6285 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -629,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + lpinyin: + dependency: "direct main" + description: + name: lpinyin + sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a" + url: "https://pub.dev" + source: hosted + version: "2.0.3" matcher: dependency: transitive description: @@ -860,6 +868,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + reorderables: + dependency: "direct main" + description: + name: reorderables + sha256: "004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77" + url: "https://pub.dev" + source: hosted + version: "0.6.0" screen_retriever: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7bcc9125c..f4f077c5c 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.51+202408051 +version: 0.8.51+202408111 environment: sdk: '>=3.1.0 <4.0.0' @@ -44,6 +44,7 @@ dependencies: re_editor: ^0.3.1 re_highlight: ^0.0.3 archive: ^3.6.1 + lpinyin: ^2.0.3 dev_dependencies: flutter_test: sdk: flutter diff --git a/test/command_test.dart b/test/command_test.dart index a6aeabc65..0267bc8d3 100644 --- a/test/command_test.dart +++ b/test/command_test.dart @@ -2,7 +2,18 @@ import 'dart:io'; -void main() async { +import 'package:fl_clash/common/other.dart'; +import 'package:lpinyin/lpinyin.dart'; + +void main() { + print(PinyinHelper.getPinyin("ABC")); + print(PinyinHelper.getPinyin("阿里巴巴")); + + print('a'.compareTo('B')); + print('A'.compareTo('B')); +} + +startService() async { // 定义服务器将要监听的地址和端口 final host = InternetAddress.anyIPv4; // 监听所有网络接口 const port = 8080; // 使用 8080 端口 From 5ef020db73afcdb532d33e5943215526895947a8 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 11 Aug 2024 17:07:34 +0800 Subject: [PATCH 10/25] Update timeout time --- lib/common/request.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/common/request.dart b/lib/common/request.dart index 22a25f397..b1f1d1f59 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -54,7 +54,7 @@ class Request { ), ) .timeout( - httpTimeoutDuration * 2, + httpTimeoutDuration * 6, ); return response; } From 68dd262fef672b752046bf822e9556be3d023382 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 11 Aug 2024 17:09:31 +0800 Subject: [PATCH 11/25] Update version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f4f077c5c..9b5a376f8 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.51+202408111 +version: 0.8.52+202408111 environment: sdk: '>=3.1.0 <4.0.0' From 813198a21dadcdd27e73a038165cfc33b45428ef Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 11 Aug 2024 17:45:57 +0800 Subject: [PATCH 12/25] Update flutter version --- lib/common/color.dart | 1 - pubspec.lock | 34 +++++++++++++--------------------- pubspec.yaml | 1 - 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/common/color.dart b/lib/common/color.dart index bdad4a49b..86002bf19 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -29,7 +29,6 @@ extension ColorSchemeExtension on ColorScheme { ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack ? copyWith( surface: Colors.black, - background: Colors.black, surfaceContainer: surfaceContainer.darken(0.05), ) : this; diff --git a/pubspec.lock b/pubspec.lock index e7c8b6285..68c727208 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -593,18 +593,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -646,13 +646,13 @@ packages: source: hosted version: "0.12.16+1" material_color_utilities: - dependency: "direct main" + dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" menu_base: dependency: transitive description: @@ -665,10 +665,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -868,14 +868,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" - reorderables: - dependency: "direct main" - description: - name: reorderables - sha256: "004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77" - url: "https://pub.dev" - source: hosted - version: "0.6.0" screen_retriever: dependency: transitive description: @@ -1037,10 +1029,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: @@ -1141,10 +1133,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9b5a376f8..db5f49b0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: country_flags: ^2.2.0 win32: ^5.5.1 ffi: ^2.1.2 - material_color_utilities: ^0.8.0 re_editor: ^0.3.1 re_highlight: ^0.0.3 archive: ^3.6.1 From 8dafe3b0ec98b92c311fb702b560bab0a477462a Mon Sep 17 00:00:00 2001 From: chen08209 Date: Tue, 13 Aug 2024 21:42:23 +0800 Subject: [PATCH 13/25] Support profiles sort Support windows country flags display Optimize proxies page and profiles page columns --- .github/workflows/build.yml | 2 +- assets/fonts/Twemoji.Mozilla.ttf | Bin 0 -> 1454532 bytes lib/application.dart | 1 - lib/common/color.dart | 1 + lib/common/dav_client.dart | 1 + lib/common/other.dart | 25 ++- lib/common/request.dart | 12 +- lib/controller.dart | 2 +- lib/enum/enum.dart | 2 + .../dashboard/network_detection.dart | 44 ++++- lib/fragments/profiles/profiles.dart | 184 ++++++++++++++++-- lib/fragments/proxies/card.dart | 28 ++- lib/fragments/proxies/common.dart | 6 +- lib/fragments/proxies/list.dart | 8 +- lib/fragments/proxies/setting.dart | 38 ++-- lib/fragments/proxies/tab.dart | 5 +- lib/l10n/arb/intl_en.arb | 7 +- lib/l10n/arb/intl_zh_CN.arb | 7 +- lib/l10n/intl/messages_en.dart | 5 + lib/l10n/intl/messages_zh_CN.dart | 5 + lib/l10n/l10n.dart | 50 +++++ lib/models/config.dart | 35 ++-- lib/models/generated/config.g.dart | 12 +- lib/models/generated/selector.freezed.dart | 183 ++--------------- lib/models/selector.dart | 10 +- lib/pages/home.dart | 1 - lib/widgets/text.dart | 61 ++++++ pubspec.lock | 66 ++----- pubspec.yaml | 10 +- 29 files changed, 484 insertions(+), 327 deletions(-) create mode 100644 assets/fonts/Twemoji.Mozilla.ttf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2483099b4..adeccdf01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,7 +87,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' + flutter-version-file: pubspec.yaml channel: 'stable' cache: true diff --git a/assets/fonts/Twemoji.Mozilla.ttf b/assets/fonts/Twemoji.Mozilla.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ac8ee3bd4c630efb9a23c558757500dac89acfb4 GIT binary patch literal 1454532 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 rK`*npzrYCQk#yTTT@@>S>~t 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