From 38221bcd10f49db126907362d363b3004f8a6306 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Mon, 5 Aug 2024 19:25:35 +0800 Subject: [PATCH] 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 | 62 +- 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, 1393 insertions(+), 645 deletions(-) create mode 100644 lib/widgets/setting.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1803f84..2483099 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 c86cd99..b3e1db1 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 41c8731..7240c3b 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 809019d..9497853 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 { - 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" + 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 - ) - } + }?.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) { + 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 fffdf84..44d4b6d 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 45056e0..0a82f49 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 externalProviders + 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") + } } 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 355b0d7..8ad4fc6 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, - } + eps := make([]ExternalProvider, 0) + for _, p := range externalProviders { + externalProvider, err := toExternalProvider(p) + if err != nil { + continue } + eps = append(eps, *externalProvider) } - 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, - } - } - } - 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 d328804..81cbcf6 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 f518d46..8f5add8 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,37 @@ class ClashCore { final externalProviderRawString = externalProviderRaw.cast().toDartString(); clashFFI.freeCString(externalProviderRaw); + if(externalProviderRawString.isEmpty) return null; return ExternalProvider.fromJson(json.decode(externalProviderRawString)); } - Future updateExternalProvider({ + 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 providerType, + required String data, }) { final completer = Completer(); final receiver = ReceivePort(); @@ -175,14 +199,34 @@ class ClashCore { } }); final providerNameChar = providerName.toNativeUtf8().cast(); - final providerTypeChar = providerType.toNativeUtf8().cast(); - clashFFI.updateExternalProvider( + 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, + }) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final providerNameChar = providerName.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 e081ea5..366dc76 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 a79a283..d9ce9c6 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 9056243..3b01298 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 f9746e6..667cbb5 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 857be5c..22a25f3 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 3dcc2d5..b83c89e 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 5f86d4f..ac74e5d 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 d5b9e44..019e433 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 6d1b493..e7d0c05 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 cb6c2fe..722d071 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 b656045..d01a48d 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 0a53ace..a379093 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 6113219..f7e1aa8 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 46dfca6..a78d9d1 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 dd439cf..d78c75b 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 c884455..3d0a6e0 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 d8a26b6..1736b1b 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 d6a651f..86a9e99 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 43156a6..abdcfc7 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 7a925b6..b106628 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 885c395..457f4cd 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 99e2e58..78e0f67 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 796b6fa..66ba860 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 ef0dbcb..5879be1 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 071ebd3..a26da67 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 11147fe..ea1da72 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 18e0567..8eb42c9 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 dff1c14..de7626b 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 8109b08..5b479a5 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 97b539c..3d212e7 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 1405f1d..ff2d62a 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 abf406f..31eb642 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 0d18aab..06cf90c 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 d55cdfc..df1ef60 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 ca212ad..88d0cff 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 e38736b..96897d6 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 0000000..111252c --- /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 e1ab8a9..2aabbcf 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 3775108..e7c8b62 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 7bcc912..f4f077c 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 a6aeabc..0267bc8 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 端口